Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
469 changes: 459 additions & 10 deletions README.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions docs/examples/db.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,18 @@ for _, user in ipairs(results) do
print("User:", user.id, user.name, user.email)
end

-- Using transactions
db:transaction(function()
db:execute("INSERT INTO users (name, email) VALUES (?, ?);", "Bob", "bob@example.com")
db:execute("INSERT INTO users (name, email) VALUES (?, ?);", "Carol", "carol@example.com")
-- Both inserts succeed or both fail
end)

-- Manual transaction control
db:beginTransaction()
db:execute("INSERT INTO users (name, email) VALUES (?, ?);", "Dave", "dave@example.com")
db:commit()
-- Or db:rollback() to undo

-- Close the database connection
db:close()
31 changes: 28 additions & 3 deletions docs/examples/lumo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,38 @@ end
local success = found_user:update({ name = "Alice Updated" })
print("Update Successful:", success)

-- Retrieve all users
-- Retrieve all users (returns a Collection)
local users = User:all()
print("All Users:")
for _, u in ipairs(users) do
print("Total Users:", #users)

-- Collections support array-like access and iteration
for i, u in ipairs(users) do
print(u.id, u.name, u.email)
end

-- Collection methods for functional programming
local names = users:map(function(u) return u.name end)
local emails = users:pluck("email")
local sorted = users:sortBy("name", true)

print("Names:", table.concat(names:toArray(), ", "))
print("First user:", users:first().name)
print("Last user:", users:last().name)

-- Advanced queries with collections
local admins = User:where("role", "=", "admin")
:orWhere("role", "=", "moderator")
:get()

local activeUsers = User:whereIn("status", {"active", "premium"}):get()

-- Transactions
Lumo.db:transaction(function()
User:create({ name = "Bob", email = "bob@example.com" })
User:create({ name = "Carol", email = "carol@example.com" })
-- If any fails, both are rolled back
end)

-- Delete a user
local deleted = found_user:delete()
print("Delete Successful:", deleted)
Expand Down
27 changes: 27 additions & 0 deletions docs/examples/query_builder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@ if user then
print("User Found:", user.id, user.name, user.email)
end

-- Query with multiple conditions
local users = QueryBuilder:new("users")
:where("name", "LIKE", "%Alice%")
:where("id", ">", 5)
:orderBy("name", "ASC")
:get()

print("Found users:", #users)
for i, user in ipairs(users) do
print(user.id, user.name, user.email)
end

-- OR conditions
local users_or = QueryBuilder:new("users")
:where("id", "=", 1)
:orWhere("id", "=", 2)
:get()

print("Users with OR condition:", #users_or)

-- IN queries
local specific_users = QueryBuilder:new("users")
:whereIn("id", {user_id, 2, 3})
:get()

print("Users with IN clause:", #specific_users)

-- Update a user
local updated = QueryBuilder:new("users"):where("id", "=", user_id):update({ name = "Alice Updated" })
print("Update Successful:", updated)
Expand Down
29 changes: 24 additions & 5 deletions docs/examples/relationships.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,35 @@ local post1 = Post:create({ title = "First Post", user_id = user.id })
local post2 = Post:create({ title = "Second Post", user_id = user.id })
print("Inserted Posts:", post1.id, post1.title, "|", post2.id, post2.title)

-- Retrieve user's posts
-- Retrieve user's posts (returns Collection of Post model instances)
local posts = user:posts()
print("User's Posts:")
for _, post in ipairs(posts) do
print(post.id, post.title)
print("User has", #posts, "posts")

-- All posts are Model instances, so you can call methods on them
for i, post in ipairs(posts) do
print("Post:", post.id, post.title)
-- Update the post
post:update({ title = post.title .. " (Updated)" })
end

-- Retrieve post's user
-- Collection methods
local titles = posts:map(function(post) return post.title end)
print("Titles:", table.concat(titles:toArray(), ", "))

-- Retrieve post's user (returns User model instance)
local post_user = post1:user()
print("Post belongs to User:", post_user.id, post_user.name)

-- Cascade delete example
User.__cascadeDelete = { "posts" }

-- When we delete the user, all their posts are automatically deleted
local user_to_delete = User:find(user.id)
user_to_delete:delete() -- This also deletes all posts

-- Verify posts are deleted
local remaining_posts = Post:all()
print("Remaining posts after cascade delete:", #remaining_posts)

-- Close the database connection
db:close()
33 changes: 33 additions & 0 deletions rockspecs/lumo-orm-1.1-0.rockspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package = "lumo-orm"
version = "1.1-0"
source = {
url = "https://github.com/bhhaskin/lua-lumo-orm/archive/refs/tags/v1.1-0.tar.gz",
dir = "lua-lumo-orm-1.1-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 <bhhaskin@bitsofsimplicity.com>"
}
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",
["lumo.seeder"] = "src/lumo/seeder.lua",
}
}
10 changes: 2 additions & 8 deletions spec/relationships_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,8 @@ describe("Model Relationships", function()

-- Define relationships inside `before_each`
function User:posts()
local results = self:hasMany(Post, "user_id")
local instances = {}
for _, row in ipairs(results) do
table.insert(instances, Post:new(row)) -- Convert table rows into Post model instances
end
return instances
end

return self:hasMany(Post, "user_id")
end

function Post:user()
return self:belongsTo(User, "user_id")
Expand Down
15 changes: 14 additions & 1 deletion src/lumo/collection.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
local Collection = {}
Collection.__index = Collection

-- Support # operator for length
Collection.__len = function(self)
return #self.items
end

-- Support numeric indexing and method access
Collection.__index = function(self, key)
if type(key) == "number" then
return self.items[key]
else
return Collection[key]
end
end

-- Create a new collection
function Collection:new(items)
Expand Down
75 changes: 66 additions & 9 deletions src/lumo/db.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ DB.__index = DB
function DB.connect(path)
local instance = setmetatable({}, DB)
instance.db = sqlite3.open(path)
instance.in_transaction = false
return instance
end

Expand All @@ -16,18 +17,71 @@ function DB:close()
end
end

-- Begin a transaction
function DB:beginTransaction()
if self.in_transaction then
error("Transaction already in progress")
end
local success = self:execute("BEGIN TRANSACTION")
if success then
self.in_transaction = true
end
return success
end

-- Commit a transaction
function DB:commit()
if not self.in_transaction then
error("No transaction in progress")
end
local success = self:execute("COMMIT")
if success then
self.in_transaction = false
end
return success
end

-- Rollback a transaction
function DB:rollback()
if not self.in_transaction then
error("No transaction in progress")
end
local success = self:execute("ROLLBACK")
if success then
self.in_transaction = false
end
return success
end

-- Execute a function within a transaction
function DB:transaction(fn)
local success, err = pcall(function()
self:beginTransaction()
fn()
self:commit()
end)

if not success then
if self.in_transaction then
self:rollback()
end
error(err)
end

return true
end

-- Run a query and return rows
function DB:query(sql, ...)
local stmt = self.db:prepare(sql)
if not stmt then
print("[SQLite Error] Failed to prepare SQL:", sql, self.db:errmsg()) -- Debug log
return nil
error("[SQLite Error] Failed to prepare SQL: " .. sql .. " - " .. self.db:errmsg())
end

if stmt:bind_values(...) ~= sqlite3.OK then
print("[SQLite Error] Failed to bind values:", self.db:errmsg()) -- Debug log
local err = self.db:errmsg()
stmt:finalize()
return nil
error("[SQLite Error] Failed to bind values: " .. err)
end

local results = {}
Expand All @@ -43,20 +97,23 @@ end
function DB:execute(sql, ...)
local stmt = self.db:prepare(sql)
if not stmt then
print("[SQLite Error] Failed to execute SQL:", sql, self.db:errmsg()) -- Debug log
return false, self.db:errmsg()
error("[SQLite Error] Failed to execute SQL: " .. sql .. " - " .. self.db:errmsg())
end

if stmt:bind_values(...) ~= sqlite3.OK then
print("[SQLite Error] Failed to bind values:", self.db:errmsg()) -- Debug log
local err = self.db:errmsg()
stmt:finalize()
return false, self.db:errmsg()
error("[SQLite Error] Failed to bind values: " .. err)
end

local res = stmt:step()
stmt:finalize()

return res == sqlite3.DONE
if res ~= sqlite3.DONE then
error("[SQLite Error] Statement execution failed: " .. self.db:errmsg())
end

return true
end

-- Get the last inserted ID
Expand Down
Loading