From 5d0d0510d1c1c09e243fff1240b03338bf8fe48b Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 10:29:25 -0800 Subject: [PATCH 1/7] added collections --- docs/examples/collection.lua | 36 +++++++++++++++++++++ spec/collection_spec.lua | 55 +++++++++++++++++++++++++++++++ spec/model_spec.lua | 47 ++++++++++++++++++++------- src/lumo/collection.lua | 63 ++++++++++++++++++++++++++++++++++++ src/lumo/model.lua | 31 ++++++++++++++---- 5 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 docs/examples/collection.lua create mode 100644 spec/collection_spec.lua create mode 100644 src/lumo/collection.lua diff --git a/docs/examples/collection.lua b/docs/examples/collection.lua new file mode 100644 index 0000000..c1bcb41 --- /dev/null +++ b/docs/examples/collection.lua @@ -0,0 +1,36 @@ +local Collection = require("lumo.collection") + +-- Sample data +local users = Collection:new({ + { id = 1, name = "Alice", email = "alice@example.com" }, + { id = 2, name = "Bob", email = "bob@example.com" }, + { id = 3, name = "Charlie", email = "charlie@example.com" }, + { id = 4, name = "Alice", email = "alice2@example.com" } +}) + +print("Total users:", users:count()) + +-- Filtering collection: Get users with the name "Alice" +local alices = users:filter(function(user) + return user.name == "Alice" +end) + +print("Users named Alice:", alices:count()) +for _, user in ipairs(alices:all()) do + print(user.id, user.name, user.email) +end + +-- Plucking emails +local emails = users:pluck("email") +print("All emails:", table.concat(emails, ", ")) + +-- Mapping collection: Convert names to uppercase +local uppercased = users:map(function(user) + user.name = string.upper(user.name) + return user +end) + +print("Uppercased names:") +for _, user in ipairs(uppercased:all()) do + print(user.id, user.name, user.email) +end \ No newline at end of file diff --git a/spec/collection_spec.lua b/spec/collection_spec.lua new file mode 100644 index 0000000..d6c2685 --- /dev/null +++ b/spec/collection_spec.lua @@ -0,0 +1,55 @@ +local Collection = require("lumo.collection") +local busted = require("busted") + +describe("Collection", function() + it("should create an empty collection", function() + local collection = Collection:new() + assert.is_not_nil(collection) + assert.are.equal(0, collection:count()) + end) + + it("should store and retrieve items", function() + local collection = Collection:new({1, 2, 3}) + assert.are.equal(3, collection:count()) + assert.are.same({1, 2, 3}, collection.items) + end) + + it("should check if it is an instance of Collection", function() + local collection = Collection:new({1, 2, 3}) + assert.is_true(collection:isInstanceOf(Collection)) + end) + + it("should filter items", function() + local collection = Collection:new({1, 2, 3, 4, 5}) + local filtered = collection:filter(function(item) return item % 2 == 0 end) + assert.are.equal(2, filtered:count()) + assert.are.same({2, 4}, filtered.items) + end) + + it("should map over items", function() + local collection = Collection:new({1, 2, 3}) + local mapped = collection:map(function(item) return item * 2 end) + assert.are.same({2, 4, 6}, mapped.items) + end) + + it("should retrieve a specific item using pluck", function() + local data = { + {id = 1, name = "Alice"}, + {id = 2, name = "Bob"}, + {id = 3, name = "Charlie"} + } + local collection = Collection:new(data) + local names = collection:pluck("name") + assert.are.same({"Alice", "Bob", "Charlie"}, names) + end) + + it("should retrieve the first item", function() + local collection = Collection:new({10, 20, 30}) + assert.are.equal(10, collection:first()) + end) + + it("should retrieve the last item", function() + local collection = Collection:new({10, 20, 30}) + assert.are.equal(30, collection:last()) + end) +end) \ No newline at end of file diff --git a/spec/model_spec.lua b/spec/model_spec.lua index 9a6b254..9067eb6 100644 --- a/spec/model_spec.lua +++ b/spec/model_spec.lua @@ -1,5 +1,6 @@ local DB = require("lumo.db") local Model = require("lumo.model") +local Collection = require("lumo.collection") local busted = require("busted") -- Define a test model @@ -40,15 +41,15 @@ describe("Model", function() assert.are.equal("alice@example.com", found.email) end) - it("should update a record", function() - local user = User:create({ name = "Alice", email = "alice@example.com" }) - local success = user:update({ email = "alice@new.com" }) - assert.is_true(success) + -- it("should update a record", function() + -- local user = User:create({ name = "Alice", email = "alice@example.com" }) + -- user.email = "alice@new.com" + -- user:save() - local updated = User:find(user.id) - assert.is_not_nil(updated) - assert.are.equal("alice@new.com", updated.email) - end) + -- local updated = User:find(user.id) + -- assert.is_not_nil(updated) + -- assert.are.equal("alice@new.com", updated.email) + -- end) it("should delete a record", function() local user = User:create({ name = "Alice", email = "alice@example.com" }) @@ -59,11 +60,35 @@ describe("Model", function() assert.is_nil(deleted) end) - it("should retrieve all records", function() + it("should retrieve all records as a Collection", function() User:create({ name = "Alice", email = "alice@example.com" }) User:create({ name = "Bob", email = "bob@example.com" }) local users = User:all() - assert.are.equal(2, #users) + assert.are.equal(2, users:count()) + assert.is_true(users:isInstanceOf(Collection)) + end) + + it("should map over a Collection", function() + User:create({ name = "Alice", email = "alice@example.com" }) + User:create({ name = "Bob", email = "bob@example.com" }) + + local emails = User:all():map(function(user) + return user.email + end):toArray() + + assert.are.same({ "alice@example.com", "bob@example.com" }, emails) + end) + + it("should filter a Collection", function() + User:create({ name = "Alice", email = "alice@example.com" }) + User:create({ name = "Bob", email = "bob@example.com" }) + + local filtered = User:all():filter(function(user) + return user.name == "Alice" + end) + + assert.are.equal(1, filtered:count()) + assert.are.equal("Alice", filtered:first().name) end) -end) +end) \ No newline at end of file diff --git a/src/lumo/collection.lua b/src/lumo/collection.lua new file mode 100644 index 0000000..e9a2ea6 --- /dev/null +++ b/src/lumo/collection.lua @@ -0,0 +1,63 @@ +local Collection = {} +Collection.__index = Collection + +-- Create a new collection +function Collection:new(items) + local instance = setmetatable({ items = items or {} }, self) + return instance +end + +-- Map over items +function Collection:map(callback) + local results = {} + for i, item in ipairs(self.items) do + results[i] = callback(item, i) + end + return Collection:new(results) +end + +-- Filter items +function Collection:filter(callback) + local results = {} + for _, item in ipairs(self.items) do + if callback(item) then + table.insert(results, item) + end + end + return Collection:new(results) +end + +-- Get first item +function Collection:first() + return self.items[1] or nil +end + +-- Get last item +function Collection:last() + return self.items[#self.items] or nil +end + +-- Convert to plain Lua table +function Collection:toArray() + return self.items +end + +function Collection:count() + return #self.items +end + +function Collection:isInstanceOf(class) + return getmetatable(self) == class +end + +function Collection:pluck(key) + local results = {} + for _, item in ipairs(self.items) do + if type(item) == "table" and item[key] ~= nil then + table.insert(results, item[key]) + end + end + return results +end + +return Collection \ No newline at end of file diff --git a/src/lumo/model.lua b/src/lumo/model.lua index ac28e6e..e8f4564 100644 --- a/src/lumo/model.lua +++ b/src/lumo/model.lua @@ -1,5 +1,6 @@ local QueryBuilder = require("lumo.query_builder") -local Relationships = require("lumo.relationships") -- Import relationships module +local Relationships = require("lumo.relationships") +local Collection = require("lumo.collection") local Model = {} Model.__index = Model @@ -34,11 +35,12 @@ end -- Get all records function Model:all() local result = self:query():get() - local instances = {} - for _, row in ipairs(result) do - table.insert(instances, self:new(row)) - end - return instances + return Collection:new(result) -- Return as Collection +end + +function Model:get() + local result = self.query_instance:get() + return Collection:new(result) -- Return as Collection end -- Insert a new record @@ -79,6 +81,23 @@ function Model:delete() return success end +-- Fluent Query Builder Methods +function Model:where(field, operator, value) + return self:query():where(field, operator, value) +end + +function Model:orderBy(field, direction) + return self:query():orderBy(field, direction) +end + +function Model:limit(count) + return self:query():limit(count) +end + +function Model:first() + return self:query():first() +end + -- **Use Relationships from `relationships.lua`** Model.hasOne = Relationships.hasOne Model.hasMany = Relationships.hasMany From 214a00cacc161e83ea5c8882a1739974b94ca779 Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 10:45:56 -0800 Subject: [PATCH 2/7] added new functions --- spec/model_spec.lua | 90 ++++++++++++++++++-------- src/lumo/model.lua | 151 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 210 insertions(+), 31 deletions(-) diff --git a/spec/model_spec.lua b/spec/model_spec.lua index 9067eb6..f295f77 100644 --- a/spec/model_spec.lua +++ b/spec/model_spec.lua @@ -14,7 +14,7 @@ describe("Model", function() before_each(function() db = DB.connect(":memory:") -- Use in-memory SQLite database db:execute([[DROP TABLE IF EXISTS users;]]) - db:execute([[CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);]]) + db:execute([[CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT, age INTEGER DEFAULT 0);]]) Model.setDB(db) -- Ensure models have a database connection end) @@ -27,7 +27,6 @@ describe("Model", function() it("should insert a record", function() local user = User:create({ name = "Alice", email = "alice@example.com" }) - assert.is_not_nil(user) assert.is_not_nil(user.id) assert.are.equal("Alice", user.name) assert.are.equal("alice@example.com", user.email) @@ -41,15 +40,14 @@ describe("Model", function() assert.are.equal("alice@example.com", found.email) end) - -- it("should update a record", function() - -- local user = User:create({ name = "Alice", email = "alice@example.com" }) - -- user.email = "alice@new.com" - -- user:save() + it("should update a record", function() + local user = User:create({ name = "Alice", email = "alice@example.com" }) + user:update({ email = "alice@new.com" }) - -- local updated = User:find(user.id) - -- assert.is_not_nil(updated) - -- assert.are.equal("alice@new.com", updated.email) - -- end) + local updated = User:find(user.id) + assert.is_not_nil(updated) + assert.are.equal("alice@new.com", updated.email) + end) it("should delete a record", function() local user = User:create({ name = "Alice", email = "alice@example.com" }) @@ -59,7 +57,7 @@ describe("Model", function() local deleted = User:find(user.id) assert.is_nil(deleted) end) - + it("should retrieve all records as a Collection", function() User:create({ name = "Alice", email = "alice@example.com" }) User:create({ name = "Bob", email = "bob@example.com" }) @@ -69,26 +67,62 @@ describe("Model", function() assert.is_true(users:isInstanceOf(Collection)) end) - it("should map over a Collection", function() - User:create({ name = "Alice", email = "alice@example.com" }) - User:create({ name = "Bob", email = "bob@example.com" }) + it("should increment and decrement a field", function() + local user = User:create({ name = "Alice", email = "alice@example.com", age = 25 }) - local emails = User:all():map(function(user) - return user.email - end):toArray() - - assert.are.same({ "alice@example.com", "bob@example.com" }, emails) + user:increment("age", 5) + assert.are.equal(30, user.age) + + user:decrement("age", 2) + assert.are.equal(28, user.age) end) - it("should filter a Collection", function() - User:create({ name = "Alice", email = "alice@example.com" }) - User:create({ name = "Bob", email = "bob@example.com" }) - - local filtered = User:all():filter(function(user) - return user.name == "Alice" - end) + it("should find or create a record", function() + local user1 = User:find_or_create({ email = "alice@example.com", name = "Alice" }, "email") + local user2 = User:find_or_create({ email = "alice@example.com", name = "ShouldNotChange" }, "email") + + assert.are.equal(user1.id, user2.id) + assert.are.equal("Alice", user2.name) + end) + + it("should update or create a record", function() + local user1 = User:update_or_create({ email = "alice@example.com" }, { name = "Alice Updated" }) + local user2 = User:update_or_create({ email = "alice@example.com" }, { name = "Alice Final" }) + + assert.are.equal(user1.id, user2.id) + assert.are.equal("Alice Final", user2.name) + end) + + it("should use only selected fields", function() + local user = User:create({ name = "Alice", email = "alice@example.com", age = 30 }) + local selected = user:only("name", "age") + + assert.are.equal("Alice", selected.name) + assert.are.equal(30, selected.age) + assert.is_nil(selected.email) + end) + + it("should fill an existing record with new data", function() + local user = User:create({ name = "Alice", email = "alice@example.com" }) + user:fill({ email = "alice@new.com", age = 29 }) + + assert.are.equal("alice@new.com", user.email) + assert.are.equal(29, user.age) + end) + + it("should refresh a record", function() + local user = User:create({ name = "Alice", email = "alice@example.com" }) + User:update_or_create({ email = "alice@example.com" }, { name = "Alice Updated" }) + + user:refresh() + assert.are.equal("Alice Updated", user.name) + end) + + it("should check if a record exists", function() + local user = User:create({ name = "Alice", email = "alice@example.com" }) + assert.is_true(user:exists()) - assert.are.equal(1, filtered:count()) - assert.are.equal("Alice", filtered:first().name) + user:delete() + assert.is_false(user:exists()) end) end) \ No newline at end of file diff --git a/src/lumo/model.lua b/src/lumo/model.lua index e8f4564..81fb167 100644 --- a/src/lumo/model.lua +++ b/src/lumo/model.lua @@ -27,11 +27,25 @@ end function Model:find(id) local result = self:query():where("id", "=", id):limit(1):get() if #result > 0 then - return self:new(result[1]) -- Return a new instance with data + return self:new(result[1]) -- Ensure it's an instance of Model end return nil end +-- Save: Insert if new, otherwise update +function Model:save() + if self.id then + return self:update(self) + else + local id = self:query():insert(self) + if id then + self.id = id -- Assign ID to the instance + return true + end + end + return false +end + -- Get all records function Model:all() local result = self:query():get() @@ -52,8 +66,16 @@ end -- Update an existing record function Model:update(data) if not self.id then error("Cannot update a record without an ID") end + local success = self:query():where("id", "=", self.id):update(data) - return success == true + + if success then + for key, value in pairs(data) do + self[key] = value -- Update instance attributes + end + end + + return success end -- Delete a record @@ -95,7 +117,130 @@ function Model:limit(count) end function Model:first() - return self:query():first() + local result = self:query():first() + if #result > 0 then + return self:new(result[1]) -- Ensure instance of Model + end + return nil +end + +function Model:only(...) + local selected = {} + for _, key in ipairs({...}) do + selected[key] = self[key] + end + return selected +end + +function Model:fill(data) + for key, value in pairs(data) do + self[key] = value + end +end + +function Model:upsert(data, uniqueKey) + uniqueKey = uniqueKey or "id" + + -- Ensure the unique key exists in data + if not data[uniqueKey] then + error("Upsert requires a `" .. uniqueKey .. "` field in data.") + end + + -- Try to find an existing record + local existing = self:query():where(uniqueKey, "=", data[uniqueKey]):first() + + if existing then + -- Update existing record + existing:update(data) + return existing + else + -- Create new record + return self:create(data) + end +end + +function Model:increment(field, amount) + amount = amount or 1 + if self.id then + self[field] = (self[field] or 0) + amount + self:save() + end +end + +function Model:decrement(field, amount) + amount = amount or 1 + if self.id then + self[field] = (self[field] or 0) - amount + self:save() + end +end + +function Model:toTable() + local data = {} + for key, value in pairs(self) do + if key ~= "_dirty" and key ~= "_original" then + data[key] = value + end + end + return data +end + +function Model:refresh() + if not self.id then return nil end + + local refreshed = self:query():where("id", "=", self.id):first() + if refreshed then + setmetatable(refreshed, getmetatable(self)) -- Ensure it remains a model instance + for key, value in pairs(refreshed) do + self[key] = value + end + end + + return self +end + +function Model:exists() + return self.id ~= nil and self:query():where("id", "=", self.id):first() ~= nil +end + +function Model:find_or_create(data, uniqueKey) + uniqueKey = uniqueKey or "id" + + local existing = self:query():where(uniqueKey, "=", data[uniqueKey]):first() + if existing then + return existing + else + return self:create(data) + end +end + +function Model:update_or_create(findData, updateData) + local query = self:query() + + -- Apply all key-value conditions for search + for key, value in pairs(findData) do + query:where(key, "=", value) + end + + local existing = query:first() + + if existing then + -- Ensure it is a proper model instance + if not getmetatable(existing) then + existing = self:new(existing) -- Convert result to an instance + end + + -- Call update on the instance + existing:update(updateData) + return existing + else + -- Merge findData and updateData to create a new record + local newData = {} + for k, v in pairs(findData) do newData[k] = v end + for k, v in pairs(updateData) do newData[k] = v end + + return self:create(newData) + end end -- **Use Relationships from `relationships.lua`** From 9a6ef22c148798d3291a10964ef6d1c92cf9aeed Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 10:49:42 -0800 Subject: [PATCH 3/7] updated --- docs/examples/model.lua | 80 ++++++++++++++++++++++++++--------------- spec/model_spec.lua | 31 ++++++++++++++++ 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/docs/examples/model.lua b/docs/examples/model.lua index afa7682..269e1b8 100644 --- a/docs/examples/model.lua +++ b/docs/examples/model.lua @@ -1,44 +1,66 @@ --- Example usage of lumo.model - local DB = require("lumo.db") local Model = require("lumo.model") --- Connect to a SQLite database -local db = DB.connect("example.sqlite") +-- Connect to an in-memory SQLite database +local db = DB.connect(":memory:") Model.setDB(db) +-- Create a test table +db:execute([[ + CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + email TEXT, + age INTEGER + ); +]]) + -- Define a User model local User = setmetatable({}, Model) User.__index = User User.table = "users" --- Create a users table -local create_table_sql = [[ - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - email TEXT UNIQUE NOT NULL - ); -]] -db:execute(create_table_sql) - --- Insert a user -local user = User:create({ name = "Alice", email = "alice@example.com" }) -print("Inserted User:", user.id, user.name, user.email) +-- **Create a new user** +local user = User:create({ name = "Alice", email = "alice@example.com", age = 25 }) +print("Created User:", user.id, user.name, user.email, user.age) --- Find a user by ID -local found_user = User:find(user.id) -if found_user then - print("User Found:", found_user.id, found_user.name, found_user.email) +-- **Find a user by ID** +local foundUser = User:find(user.id) +if foundUser then + print("Found User:", foundUser.id, foundUser.name, foundUser.email) end --- Update a user -local success = found_user:update({ name = "Alice Updated" }) -print("Update Successful:", success) +-- **Update a user** +foundUser.email = "alice@newdomain.com" +foundUser:save() +print("Updated Email:", foundUser.email) + +-- **Increment age** +foundUser:increment("age", 1) +print("Incremented Age:", foundUser.age) + +-- **Retrieve all users as a collection** +local users = User:all() +print("Total Users:", users:count()) + +-- **Use where queries** +local filteredUsers = User:where("age", ">", 20):get() +print("Users older than 20:", filteredUsers:count()) + +-- **Find or create a user** +local existingOrNewUser = User:find_or_create({ email = "bob@example.com" }, { name = "Bob", age = 30 }) +print("Find or Create User:", existingOrNewUser.id, existingOrNewUser.name) + +-- **Update or create a user** +local updatedOrCreatedUser = User:update_or_create({ email = "bob@example.com" }, { age = 35 }) +print("Update or Create User:", updatedOrCreatedUser.id, updatedOrCreatedUser.age) --- Delete a user -local deleted = found_user:delete() -print("Delete Successful:", deleted) +-- **Delete a user** +local deleteSuccess = foundUser:delete() +print("User deleted:", deleteSuccess) --- Close the database connection -db:close() +-- **Verify deletion** +local checkUser = User:find(user.id) +if not checkUser then + print("User successfully deleted.") +end \ No newline at end of file diff --git a/spec/model_spec.lua b/spec/model_spec.lua index f295f77..ca43f5f 100644 --- a/spec/model_spec.lua +++ b/spec/model_spec.lua @@ -49,6 +49,37 @@ describe("Model", function() assert.are.equal("alice@new.com", updated.email) end) + it("should update a record by modifying a property and calling save", function() + local user = User:create({ name = "Alice", email = "alice@example.com" }) + + -- Modify an attribute directly + user.email = "updated@example.com" + user:save() -- Save the updated value + + local updated = User:find(user.id) + assert.is_not_nil(updated) + assert.are.equal("updated@example.com", updated.email) + end) + + it("should save a new record", function() + local user = User:new({ name = "Alice", email = "alice@example.com" }) + user:save() + assert.is_not_nil(user.id) + + local found = User:find(user.id) + assert.are.equal("Alice", found.name) + assert.are.equal("alice@example.com", found.email) + end) + + it("should update an existing record using save", function() + local user = User:create({ name = "Alice", email = "alice@example.com" }) + user.name = "Alice Updated" + user:save() + + local updated = User:find(user.id) + assert.are.equal("Alice Updated", updated.name) + end) + it("should delete a record", function() local user = User:create({ name = "Alice", email = "alice@example.com" }) local success = user:delete() From 7d7d2e432bbc09963c83ebd20f5ccb313180abe6 Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 10:51:28 -0800 Subject: [PATCH 4/7] few fixes --- src/lumo/model.lua | 76 ++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 53 deletions(-) diff --git a/src/lumo/model.lua b/src/lumo/model.lua index 81fb167..9b6f417 100644 --- a/src/lumo/model.lua +++ b/src/lumo/model.lua @@ -14,7 +14,10 @@ end -- Constructor (Create a new model instance) function Model:new(data) - local instance = setmetatable(data or {}, self) + local instance = setmetatable({}, self) + for key, value in pairs(data or {}) do + instance[key] = value + end return instance end @@ -26,7 +29,7 @@ end -- Find a record by ID function Model:find(id) local result = self:query():where("id", "=", id):limit(1):get() - if #result > 0 then + if result and #result > 0 then return self:new(result[1]) -- Ensure it's an instance of Model end return nil @@ -35,9 +38,16 @@ end -- Save: Insert if new, otherwise update function Model:save() if self.id then - return self:update(self) + -- Only update fields that exist in the table + local data = {} + for key, value in pairs(self) do + if type(value) ~= "function" and key ~= "_dirty" and key ~= "_original" then + data[key] = value + end + end + return self:update(data) else - local id = self:query():insert(self) + local id = self:query():insert(self:toTable()) if id then self.id = id -- Assign ID to the instance return true @@ -53,7 +63,7 @@ function Model:all() end function Model:get() - local result = self.query_instance:get() + local result = self:query():get() return Collection:new(result) -- Return as Collection end @@ -66,39 +76,18 @@ end -- Update an existing record function Model:update(data) if not self.id then error("Cannot update a record without an ID") end - local success = self:query():where("id", "=", self.id):update(data) - if success then for key, value in pairs(data) do self[key] = value -- Update instance attributes end end - return success end -- Delete a record function Model:delete() if not self.id then error("Cannot delete a record without an ID") end - - -- Run cascade delete hooks (if defined in model) - if self.__cascadeDelete then - for _, relation in ipairs(self.__cascadeDelete) do - if type(self[relation]) == "function" then - local related_records = self[relation](self) -- Fetch related records - if type(related_records) == "table" then - for _, record in ipairs(related_records) do - if record and record.delete then - record:delete() -- Ensure each related record is deleted - end - end - end - end - end - end - - -- Delete the record local success = self:query():where("id", "=", self.id):delete() return success end @@ -118,8 +107,8 @@ end function Model:first() local result = self:query():first() - if #result > 0 then - return self:new(result[1]) -- Ensure instance of Model + if result then + return self:new(result) -- Ensure instance of Model end return nil end @@ -140,21 +129,14 @@ end function Model:upsert(data, uniqueKey) uniqueKey = uniqueKey or "id" - - -- Ensure the unique key exists in data if not data[uniqueKey] then error("Upsert requires a `" .. uniqueKey .. "` field in data.") end - - -- Try to find an existing record local existing = self:query():where(uniqueKey, "=", data[uniqueKey]):first() - if existing then - -- Update existing record existing:update(data) return existing else - -- Create new record return self:create(data) end end @@ -162,23 +144,23 @@ end function Model:increment(field, amount) amount = amount or 1 if self.id then - self[field] = (self[field] or 0) + amount - self:save() + local new_value = (self[field] or 0) + amount + self:update({ [field] = new_value }) -- Explicit update end end function Model:decrement(field, amount) amount = amount or 1 if self.id then - self[field] = (self[field] or 0) - amount - self:save() + local new_value = (self[field] or 0) - amount + self:update({ [field] = new_value }) -- Explicit update end end function Model:toTable() local data = {} for key, value in pairs(self) do - if key ~= "_dirty" and key ~= "_original" then + if type(value) ~= "function" and key ~= "_dirty" and key ~= "_original" then data[key] = value end end @@ -187,7 +169,6 @@ end function Model:refresh() if not self.id then return nil end - local refreshed = self:query():where("id", "=", self.id):first() if refreshed then setmetatable(refreshed, getmetatable(self)) -- Ensure it remains a model instance @@ -195,7 +176,6 @@ function Model:refresh() self[key] = value end end - return self end @@ -205,7 +185,6 @@ end function Model:find_or_create(data, uniqueKey) uniqueKey = uniqueKey or "id" - local existing = self:query():where(uniqueKey, "=", data[uniqueKey]):first() if existing then return existing @@ -216,29 +195,20 @@ end function Model:update_or_create(findData, updateData) local query = self:query() - - -- Apply all key-value conditions for search for key, value in pairs(findData) do query:where(key, "=", value) end - local existing = query:first() - if existing then - -- Ensure it is a proper model instance if not getmetatable(existing) then existing = self:new(existing) -- Convert result to an instance end - - -- Call update on the instance existing:update(updateData) return existing else - -- Merge findData and updateData to create a new record local newData = {} for k, v in pairs(findData) do newData[k] = v end for k, v in pairs(updateData) do newData[k] = v end - return self:create(newData) end end @@ -252,4 +222,4 @@ Model.with = Relationships.with Model.cascadeDelete = Relationships.cascadeDelete Model.cascadeDeletePivot = Relationships.cascadeDeletePivot -return Model +return Model \ No newline at end of file From bfbf12d1ffd60eecaedb5e2df9be9a08bbb43be6 Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 10:55:19 -0800 Subject: [PATCH 5/7] updates to collections --- docs/examples/collection.lua | 93 +++++++++++++++++-------- spec/collection_spec.lua | 130 +++++++++++++++++++++++++---------- src/lumo/collection.lua | 68 +++++++++++++++--- 3 files changed, 220 insertions(+), 71 deletions(-) diff --git a/docs/examples/collection.lua b/docs/examples/collection.lua index c1bcb41..0c52ed1 100644 --- a/docs/examples/collection.lua +++ b/docs/examples/collection.lua @@ -1,36 +1,75 @@ local Collection = require("lumo.collection") -- Sample data -local users = Collection:new({ - { id = 1, name = "Alice", email = "alice@example.com" }, - { id = 2, name = "Bob", email = "bob@example.com" }, - { id = 3, name = "Charlie", email = "charlie@example.com" }, - { id = 4, name = "Alice", email = "alice2@example.com" } -}) - -print("Total users:", users:count()) - --- Filtering collection: Get users with the name "Alice" -local alices = users:filter(function(user) - return user.name == "Alice" -end) +local users = { + { id = 1, name = "Alice", age = 28 }, + { id = 2, name = "Bob", age = 35 }, + { id = 3, name = "Charlie", age = 24 }, + { id = 4, name = "David", age = 30 } +} -print("Users named Alice:", alices:count()) -for _, user in ipairs(alices:all()) do - print(user.id, user.name, user.email) -end +-- Create a collection +local userCollection = Collection:new(users) --- Plucking emails -local emails = users:pluck("email") -print("All emails:", table.concat(emails, ", ")) +print("---- Original Collection ----") +for _, user in ipairs(userCollection:toArray()) do + print(user.id, user.name, user.age) +end --- Mapping collection: Convert names to uppercase -local uppercased = users:map(function(user) - user.name = string.upper(user.name) +-- Map: Add a new property +local updatedUsers = userCollection:map(function(user) + user.isAdult = user.age >= 18 return user end) -print("Uppercased names:") -for _, user in ipairs(uppercased:all()) do - print(user.id, user.name, user.email) -end \ No newline at end of file +print("\n---- Users with isAdult field ----") +for _, user in ipairs(updatedUsers:toArray()) do + print(user.id, user.name, user.age, "isAdult:", user.isAdult) +end + +-- Filter: Get users older than 25 +local filteredUsers = userCollection:filter(function(user) + return user.age > 25 +end) + +print("\n---- Users older than 25 ----") +for _, user in ipairs(filteredUsers:toArray()) do + print(user.id, user.name, user.age) +end + +-- First and Last +print("\nFirst user:", userCollection:first().name) +print("Last user:", userCollection:last().name) + +-- Pluck: Extract names +local names = userCollection:pluck("name") +print("\n---- User Names ----") +for _, name in ipairs(names) do + print(name) +end + +-- Sorting by age +local sortedUsers = userCollection:sortBy("age") +print("\n---- Users Sorted by Age ----") +for _, user in ipairs(sortedUsers:toArray()) do + print(user.id, user.name, user.age) +end + +-- Reverse +local reversedUsers = userCollection:reverse() +print("\n---- Users in Reverse Order ----") +for _, user in ipairs(reversedUsers:toArray()) do + print(user.id, user.name, user.age) +end + +-- Reduce: Get total age sum +local totalAge = userCollection:reduce(function(acc, user) + return acc + user.age +end, 0) +print("\nTotal age of all users:", totalAge) + +-- Check if collection contains a user older than 30 +local hasOlderUser = userCollection:contains(function(user) + return user.age > 30 +end) +print("\nContains user older than 30?", hasOlderUser) \ No newline at end of file diff --git a/spec/collection_spec.lua b/spec/collection_spec.lua index d6c2685..76dfbc9 100644 --- a/spec/collection_spec.lua +++ b/spec/collection_spec.lua @@ -2,54 +2,114 @@ local Collection = require("lumo.collection") local busted = require("busted") describe("Collection", function() - it("should create an empty collection", function() - local collection = Collection:new() - assert.is_not_nil(collection) - assert.are.equal(0, collection:count()) + + it("should create a collection", function() + local col = Collection:new({1, 2, 3}) + assert.are.equal(3, col:count()) + end) + + it("should map over a collection", function() + local col = Collection:new({1, 2, 3}) + local new_col = col:map(function(item) return item * 2 end) + + assert.are.same({2, 4, 6}, new_col:toArray()) + end) + + it("should filter a collection", function() + local col = Collection:new({1, 2, 3, 4, 5}) + local filtered = col:filter(function(item) return item % 2 == 0 end) + + assert.are.same({2, 4}, filtered:toArray()) + end) + + it("should get the first item", function() + local col = Collection:new({10, 20, 30}) + assert.are.equal(10, col:first()) + end) + + it("should get the last item", function() + local col = Collection:new({10, 20, 30}) + assert.are.equal(30, col:last()) + end) + + it("should convert to a plain Lua table", function() + local col = Collection:new({10, 20, 30}) + assert.are.same({10, 20, 30}, col:toArray()) end) - it("should store and retrieve items", function() - local collection = Collection:new({1, 2, 3}) - assert.are.equal(3, collection:count()) - assert.are.same({1, 2, 3}, collection.items) + it("should count the number of items", function() + local col = Collection:new({1, 2, 3, 4}) + assert.are.equal(4, col:count()) end) - it("should check if it is an instance of Collection", function() - local collection = Collection:new({1, 2, 3}) - assert.is_true(collection:isInstanceOf(Collection)) + it("should check if an instance is a Collection", function() + local col = Collection:new({1, 2, 3}) + assert.is_true(col:isInstanceOf(Collection)) end) - it("should filter items", function() - local collection = Collection:new({1, 2, 3, 4, 5}) - local filtered = collection:filter(function(item) return item % 2 == 0 end) - assert.are.equal(2, filtered:count()) - assert.are.same({2, 4}, filtered.items) + it("should pluck a field from a collection of tables", function() + local col = Collection:new({ + { id = 1, name = "Alice" }, + { id = 2, name = "Bob" }, + { id = 3, name = "Charlie" } + }) + + local names = col:pluck("name") + assert.are.same({"Alice", "Bob", "Charlie"}, names:toArray()) end) - it("should map over items", function() - local collection = Collection:new({1, 2, 3}) - local mapped = collection:map(function(item) return item * 2 end) - assert.are.same({2, 4, 6}, mapped.items) + it("should iterate over each item", function() + local col = Collection:new({1, 2, 3}) + local sum = 0 + col:each(function(item) sum = sum + item end) + + assert.are.equal(6, sum) end) - it("should retrieve a specific item using pluck", function() - local data = { - {id = 1, name = "Alice"}, - {id = 2, name = "Bob"}, - {id = 3, name = "Charlie"} - } - local collection = Collection:new(data) - local names = collection:pluck("name") - assert.are.same({"Alice", "Bob", "Charlie"}, names) + it("should check if collection contains a matching item", function() + local col = Collection:new({1, 2, 3, 4}) + assert.is_true(col:contains(function(item) return item == 3 end)) + assert.is_false(col:contains(function(item) return item == 10 end)) end) - it("should retrieve the first item", function() - local collection = Collection:new({10, 20, 30}) - assert.are.equal(10, collection:first()) + it("should reduce a collection to a single value", function() + local col = Collection:new({1, 2, 3, 4}) + local sum = col:reduce(function(acc, item) return acc + item end, 0) + + assert.are.equal(10, sum) end) - it("should retrieve the last item", function() - local collection = Collection:new({10, 20, 30}) - assert.are.equal(30, collection:last()) + it("should sort a collection by a key", function() + local col = Collection:new({ + { id = 2, name = "Bob" }, + { id = 1, name = "Alice" }, + { id = 3, name = "Charlie" } + }) + + local sorted = col:sortBy("id") + local sorted_ids = sorted:pluck("id"):toArray() + + assert.are.same({1, 2, 3}, sorted_ids) end) + + it("should sort a collection by a key in descending order", function() + local col = Collection:new({ + { id = 2, name = "Bob" }, + { id = 1, name = "Alice" }, + { id = 3, name = "Charlie" } + }) + + local sorted = col:sortBy("id", false) + local sorted_ids = sorted:pluck("id"):toArray() + + assert.are.same({3, 2, 1}, sorted_ids) + end) + + it("should reverse the collection", function() + local col = Collection:new({1, 2, 3, 4}) + local reversed = col:reverse() + + assert.are.same({4, 3, 2, 1}, reversed:toArray()) + end) + end) \ No newline at end of file diff --git a/src/lumo/collection.lua b/src/lumo/collection.lua index e9a2ea6..7361da3 100644 --- a/src/lumo/collection.lua +++ b/src/lumo/collection.lua @@ -27,6 +27,53 @@ function Collection:filter(callback) return Collection:new(results) end +-- Iterate over each item without returning a new collection +function Collection:each(callback) + for i, item in ipairs(self.items) do + callback(item, i) + end +end + +-- Check if any item matches the given condition +function Collection:contains(predicate) + for _, item in ipairs(self.items) do + if predicate(item) then + return true + end + end + return false +end + +-- Reduce collection to a single value +function Collection:reduce(callback, initial) + local accumulator = initial + for i, item in ipairs(self.items) do + accumulator = callback(accumulator, item, i) + end + return accumulator +end + +-- Sort collection based on a key +function Collection:sortBy(key, ascending) + local results = { unpack(self.items) } -- Copy items + table.sort(results, function(a, b) + if ascending == false then + return a[key] > b[key] + end + return a[key] < b[key] + end) + return Collection:new(results) +end + +-- Reverse the order of the collection +function Collection:reverse() + local results = {} + for i = #self.items, 1, -1 do + table.insert(results, self.items[i]) + end + return Collection:new(results) +end + -- Get first item function Collection:first() return self.items[1] or nil @@ -42,22 +89,25 @@ function Collection:toArray() return self.items end +-- Count items in the collection function Collection:count() - return #self.items + return #self.items end +-- Check if this is an instance of Collection function Collection:isInstanceOf(class) - return getmetatable(self) == class + return getmetatable(self) == class end +-- Extract a single key from each item function Collection:pluck(key) - local results = {} - for _, item in ipairs(self.items) do - if type(item) == "table" and item[key] ~= nil then - table.insert(results, item[key]) - end - end - return results + local results = {} + for _, item in ipairs(self.items) do + if type(item) == "table" and item[key] ~= nil then + table.insert(results, item[key]) + end + end + return Collection:new(results) -- Now returns a Collection end return Collection \ No newline at end of file From c79d3f0e11885b46d02eb4a5ca2a5414be0dec45 Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 11:01:20 -0800 Subject: [PATCH 6/7] more general improvements --- src/lumo.lua | 35 ++++++++++++++++++----- src/lumo/db.lua | 31 ++++++++++++++++---- src/lumo/migrations.lua | 36 ++++++++++++++++------- src/lumo/query_builder.lua | 58 ++++++++++++++++++++++++++++---------- src/lumo/relationships.lua | 34 ++++++++++++++-------- 5 files changed, 144 insertions(+), 50 deletions(-) diff --git a/src/lumo.lua b/src/lumo.lua index d561dc1..e1b32c4 100644 --- a/src/lumo.lua +++ b/src/lumo.lua @@ -5,20 +5,41 @@ local Migrations = require("lumo.migrations") local Lumo = { _VERSION = '0.1-0', - db = nil -- Store database connection + db = nil, -- Store database connection + _db_path = nil -- Store database path for reconnecting } function Lumo.connect(db_path) - Lumo.db = DB.connect(db_path) -- Store DB connection in Lumo.db - Model.setDB(Lumo.db) - QueryBuilder.setDB(Lumo.db) - Migrations.setDB(Lumo.db) + local db, err = DB.connect(db_path) + if not db then + error("Failed to connect to the database: " .. (err or "Unknown error")) + end + Lumo.db = db + Lumo._db_path = db_path -- Store path for reconnection + Model.setDB(db) + QueryBuilder.setDB(db) + Migrations.setDB(db) + return db +end + +function Lumo.close() + if Lumo.db then + Lumo.db:close() + Lumo.db = nil + end +end + +function Lumo.getDB() + if not Lumo.db then + error("Database not connected. Call Lumo.connect() first.") + end return Lumo.db end --- Provide access to QueryBuilder function Lumo.query(table) - if not Lumo.db then error("Database not connected. Call Lumo.connect() first.") end + if not Lumo.db then + error("Database not connected. Call Lumo.connect() first.") + end return QueryBuilder:new(table) end diff --git a/src/lumo/db.lua b/src/lumo/db.lua index a2c6497..6eefaef 100644 --- a/src/lumo/db.lua +++ b/src/lumo/db.lua @@ -19,10 +19,17 @@ end -- Run a query and return rows function DB:query(sql, ...) local stmt = self.db:prepare(sql) - if not stmt then return nil end + if not stmt then + print("[SQLite Error] Failed to prepare SQL:", sql, self.db:errmsg()) -- Debug log + return nil + end + + if stmt:bind_values(...) ~= sqlite3.OK then + print("[SQLite Error] Failed to bind values:", self.db:errmsg()) -- Debug log + stmt:finalize() + return nil + end - stmt:bind_values(...) - local results = {} for row in stmt:nrows() do table.insert(results, row) @@ -35,9 +42,17 @@ end -- Execute a statement (for INSERT, UPDATE, DELETE) function DB:execute(sql, ...) local stmt = self.db:prepare(sql) - if not stmt then return false end + if not stmt then + print("[SQLite Error] Failed to execute SQL:", sql, self.db:errmsg()) -- Debug log + return false, self.db:errmsg() + end + + if stmt:bind_values(...) ~= sqlite3.OK then + print("[SQLite Error] Failed to bind values:", self.db:errmsg()) -- Debug log + stmt:finalize() + return false, self.db:errmsg() + end - stmt:bind_values(...) local res = stmt:step() stmt:finalize() @@ -46,7 +61,11 @@ end -- Get the last inserted ID function DB:lastInsertId() + if not self.db then + print("[SQLite Error] Attempted to get last insert ID on a closed database") + return nil + end return self.db:last_insert_rowid() end -return DB +return DB \ No newline at end of file diff --git a/src/lumo/migrations.lua b/src/lumo/migrations.lua index b1d7d67..2adf66e 100644 --- a/src/lumo/migrations.lua +++ b/src/lumo/migrations.lua @@ -36,7 +36,11 @@ function Migrations:apply(name, up) end -- Run the `up` migration (table creation/modification) - self.db:execute(up) + local success, err = self.db:execute(up) + if not success then + print("[Migration Error] Failed to apply migration:", name, "Error:", err) + return false + end -- Record migration as applied QueryBuilder:new("migrations"):insert({ name = name }) @@ -52,7 +56,11 @@ function Migrations:rollback(name, down) end -- Run the `down` migration (rollback) - self.db:execute(down) + local success, err = self.db:execute(down) + if not success then + print("[Migration Error] Failed to rollback migration:", name, "Error:", err) + return false + end -- Remove the migration record QueryBuilder:new("migrations"):where("name", "=", name):delete() @@ -63,30 +71,38 @@ end -- Apply all pending migrations function Migrations:migrateUp(migrations) for _, migration in ipairs(migrations) do - self:apply(migration.name, migration.up) + local success = self:apply(migration.name, migration.up) + if not success then + print("[Migration Error] Stopping migration process due to failure.") + break + end end end -- Rollback all applied migrations function Migrations:migrateDown(migrations) for i = #migrations, 1, -1 do - self:rollback(migrations[i].name, migrations[i].down) + local success = self:rollback(migrations[i].name, migrations[i].down) + if not success then + print("[Migration Error] Stopping rollback due to failure.") + break + end end end --- Create pivot table if not exists -function Migrations:createPivotTable(name, column1, column2) +-- Create pivot table with configurable foreign keys +function Migrations:createPivotTable(name, column1, refTable1, column2, refTable2) local sql = string.format([[ CREATE TABLE IF NOT EXISTS %s ( %s INTEGER NOT NULL, %s INTEGER NOT NULL, PRIMARY KEY (%s, %s), - FOREIGN KEY (%s) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (%s) REFERENCES roles(id) ON DELETE CASCADE + FOREIGN KEY (%s) REFERENCES %s(id) ON DELETE CASCADE, + FOREIGN KEY (%s) REFERENCES %s(id) ON DELETE CASCADE ); - ]], name, column1, column2, column1, column2, column1, column2) + ]], name, column1, column2, column1, column2, column1, refTable1, column2, refTable2) self.db:execute(sql) end -return Migrations +return Migrations \ No newline at end of file diff --git a/src/lumo/query_builder.lua b/src/lumo/query_builder.lua index 02c25e5..c9112fc 100644 --- a/src/lumo/query_builder.lua +++ b/src/lumo/query_builder.lua @@ -13,6 +13,8 @@ function QueryBuilder:new(table) instance.table = table instance.conditions = {} instance.params = {} + instance.order_by = nil + instance.limit_count = nil return instance end @@ -23,7 +25,19 @@ function QueryBuilder:where(field, operator, value) return self end --- Execute the query +-- Add ORDER BY clause +function QueryBuilder:orderBy(field, direction) + self.order_by = "ORDER BY " .. field .. " " .. (direction or "ASC") + return self +end + +-- Add LIMIT clause +function QueryBuilder:limit(count) + self.limit_count = "LIMIT " .. count + return self +end + +-- Execute the SELECT query function QueryBuilder:get() local sql = "SELECT * FROM " .. self.table @@ -31,9 +45,24 @@ function QueryBuilder:get() sql = sql .. " WHERE " .. table.concat(self.conditions, " AND ") end + if self.order_by then + sql = sql .. " " .. self.order_by + end + + if self.limit_count then + sql = sql .. " " .. self.limit_count + end + return self.db:query(sql, table.unpack(self.params)) end +-- Get the first record +function QueryBuilder:first() + self:limit(1) + local results = self:get() + return results and results[1] or nil +end + -- Insert a new record function QueryBuilder:insert(data) local columns, placeholders, values = {}, {}, {} @@ -52,11 +81,18 @@ function QueryBuilder:insert(data) ) local success = self.db:execute(sql, table.unpack(values)) - return success and self.db:lastInsertId() or nil + if success then + return self.db:lastInsertId() + end + return nil end -- Update existing records and return success function QueryBuilder:update(data) + if #self.conditions == 0 then + error("UPDATE queries must have a WHERE condition to prevent full table updates.") + end + local updates, values = {}, {} for column, value in pairs(data) do @@ -78,6 +114,10 @@ end -- Delete records and return success function QueryBuilder:delete() + if #self.conditions == 0 then + error("DELETE queries must have a WHERE condition to prevent full table deletion.") + end + local sql = "DELETE FROM " .. self.table if #self.conditions > 0 then @@ -87,16 +127,4 @@ function QueryBuilder:delete() return self.db:execute(sql, table.unpack(self.params)) end --- Get first record from query -function QueryBuilder:first() - local results = self:get() - return results and results[1] or nil -end - -function QueryBuilder:limit(count) - self.limit_count = "LIMIT " .. count - return self -end - - -return QueryBuilder +return QueryBuilder \ No newline at end of file diff --git a/src/lumo/relationships.lua b/src/lumo/relationships.lua index f29b171..980f439 100644 --- a/src/lumo/relationships.lua +++ b/src/lumo/relationships.lua @@ -9,7 +9,10 @@ function Relationships:hasOne(model, foreignKey, localKey) error("Invalid model passed to hasOne()") end localKey = localKey or "id" - return model:query():where(foreignKey, "=", self[localKey]):limit(1):get()[1] + foreignKey = foreignKey or model.table .. "_id" + + local result = model:query():where(foreignKey, "=", self[localKey]):limit(1):get() + return #result > 0 and result[1] or nil end -- One-to-Many relationship (with eager loading) @@ -18,6 +21,8 @@ function Relationships:hasMany(model, foreignKey, localKey) error("Invalid model passed to hasMany()") end localKey = localKey or "id" + foreignKey = foreignKey or model.table .. "_id" + return model:query():where(foreignKey, "=", self[localKey]):get() end @@ -27,7 +32,10 @@ function Relationships:belongsTo(model, foreignKey, localKey) error("Invalid model passed to belongsTo()") end localKey = localKey or "id" - return model:query():where(localKey, "=", self[foreignKey]):limit(1):get()[1] + foreignKey = foreignKey or self.table .. "_id" + + local result = model:query():where(localKey, "=", self[foreignKey]):limit(1):get() + return #result > 0 and result[1] or nil end -- Many-to-Many relationship (via pivot table with eager loading) @@ -40,13 +48,13 @@ function Relationships:belongsToMany(model, pivotTable, localKey, foreignKey) local results = QueryBuilder:new(pivotTable) :where(localKey, "=", self.id) :get() - + local relatedIDs = {} for _, row in ipairs(results) do table.insert(relatedIDs, row[foreignKey]) end - if #relatedIDs > 0 then + if next(relatedIDs) then return model:query():where("id", "IN", "(" .. table.concat(relatedIDs, ",") .. ")"):get() end return {} @@ -58,13 +66,13 @@ function Relationships:with(relations) relations = { relations } -- Allow single or multiple relationships end + local loaded = {} for _, relation in ipairs(relations) do if self[relation] and type(self[relation]) == "function" then - self[relation .. "_loaded"] = self[relation](self) + loaded[relation] = self[relation](self) end end - - return self + return loaded end -- Cascade delete for One-to-Many @@ -74,15 +82,17 @@ function Relationships:cascadeDelete(model, foreignKey) end local relatedRecords = self:hasMany(model, foreignKey) for _, record in ipairs(relatedRecords) do - record:delete() -- Delete each related record + record:delete() end end -- Cascade delete for Many-to-Many (remove pivot entries) function Relationships:cascadeDeletePivot(pivotTable, localKey) - QueryBuilder:new(pivotTable) - :where(localKey, "=", self.id) - :delete() + if self.id then + QueryBuilder:new(pivotTable) + :where(localKey, "=", self.id) + :delete() + end end -return Relationships +return Relationships \ No newline at end of file From 6de273f48c01f83ddee4ae47ba3dafde718b54fc Mon Sep 17 00:00:00 2001 From: Bryan Haskin Date: Fri, 28 Feb 2025 11:04:24 -0800 Subject: [PATCH 7/7] change version --- rockspecs/lumo-orm-1.0-0.rockspec | 33 +++++++++++++++++++++++++++++++ src/lumo.lua | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 rockspecs/lumo-orm-1.0-0.rockspec diff --git a/rockspecs/lumo-orm-1.0-0.rockspec b/rockspecs/lumo-orm-1.0-0.rockspec new file mode 100644 index 0000000..fd8af93 --- /dev/null +++ b/rockspecs/lumo-orm-1.0-0.rockspec @@ -0,0 +1,33 @@ +package = "lumo-orm" +version = "1.0-0" +source = { + url = "https://github.com/bhhaskin/lua-lumo-orm/archive/refs/tags/v1.0-0.tar.gz", + md5 = "bcadb1b22792463f7d77158a67a2af2f", + dir = "lua-lumo-orm-1.0-0" +} +description = { + summary = "A lightweight Active Record ORM for Lua with SQLite support", + detailed = [[ + Lumo-ORM provides an Eloquent-style ORM for Lua, built to work with SQLite. + It includes migrations, a query builder, relationships, and Active Record pattern support. + ]], + license = "MIT", + homepage = "https://github.com/bhhaskin/lua-lumo-orm", + maintainer = "Bryan Haskin " +} +dependencies = { + "lua >= 5.1", + "lsqlite3complete" +} +build = { + type = "builtin", + modules = { + ["lumo"] = "src/lumo.lua", + ["lumo.db"] = "src/lumo/db.lua", + ["lumo.model"] = "src/lumo/model.lua", + ["lumo.query_builder"] = "src/lumo/query_builder.lua", + ["lumo.relationships"] = "src/lumo/relationships.lua", + ["lumo.migrations"] = "src/lumo/migrations.lua", + ["lumo.collection"] = "src/lumo/collection.lua", + } +} diff --git a/src/lumo.lua b/src/lumo.lua index e1b32c4..d86ba7a 100644 --- a/src/lumo.lua +++ b/src/lumo.lua @@ -4,7 +4,7 @@ local QueryBuilder = require("lumo.query_builder") local Migrations = require("lumo.migrations") local Lumo = { - _VERSION = '0.1-0', + _VERSION = '1.0-0', db = nil, -- Store database connection _db_path = nil -- Store database path for reconnecting }