improve string library usage in the database libraries
This commit is contained in:
parent
aa340b7323
commit
041b4a517d
File diff suppressed because it is too large
Load Diff
@ -23,24 +23,19 @@ function Connection:query(query_str, ...)
|
|||||||
if not self._id then
|
if not self._id then
|
||||||
error("Connection is closed")
|
error("Connection is closed")
|
||||||
end
|
end
|
||||||
query_str = string.normalize_whitespace(query_str)
|
return moonshark.sql_query(self._id, query_str:normalize_whitespace(), ...)
|
||||||
return moonshark.sql_query(self._id, query_str, ...)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:exec(query_str, ...)
|
function Connection:exec(query_str, ...)
|
||||||
if not self._id then
|
if not self._id then
|
||||||
error("Connection is closed")
|
error("Connection is closed")
|
||||||
end
|
end
|
||||||
query_str = string.normalize_whitespace(query_str)
|
return moonshark.sql_exec(self._id, query_str:normalize_whitespace(), ...)
|
||||||
return moonshark.sql_exec(self._id, query_str, ...)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:query_row(query_str, ...)
|
function Connection:query_row(query_str, ...)
|
||||||
local results = self:query(query_str, ...)
|
local results = self:query(query_str, ...)
|
||||||
if results and #results > 0 then
|
return results and #results > 0 and results[1] or nil
|
||||||
return results[1]
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:query_value(query_str, ...)
|
function Connection:query_value(query_str, ...)
|
||||||
@ -53,77 +48,50 @@ function Connection:query_value(query_str, ...)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Enhanced transaction support with savepoints
|
|
||||||
function Connection:begin()
|
function Connection:begin()
|
||||||
local result = self:exec("BEGIN")
|
local result = self:exec("BEGIN")
|
||||||
if result then
|
if result then
|
||||||
return {
|
return {
|
||||||
conn = self,
|
conn = self,
|
||||||
active = true,
|
active = true,
|
||||||
|
|
||||||
commit = function(tx)
|
commit = function(tx)
|
||||||
if tx.active then
|
if tx.active then
|
||||||
local result = tx.conn:exec("COMMIT")
|
|
||||||
tx.active = false
|
tx.active = false
|
||||||
return result
|
return tx.conn:exec("COMMIT")
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
|
|
||||||
rollback = function(tx)
|
rollback = function(tx)
|
||||||
if tx.active then
|
if tx.active then
|
||||||
local result = tx.conn:exec("ROLLBACK")
|
|
||||||
tx.active = false
|
tx.active = false
|
||||||
return result
|
return tx.conn:exec("ROLLBACK")
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
|
|
||||||
savepoint = function(tx, name)
|
savepoint = function(tx, name)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
if name:is_blank() then error("Savepoint name cannot be empty") end
|
||||||
end
|
return tx.conn:exec("SAVEPOINT {{name}}":parse({name = name}))
|
||||||
if string.is_blank(name) then
|
|
||||||
error("Savepoint name cannot be empty")
|
|
||||||
end
|
|
||||||
return tx.conn:exec(string.template("SAVEPOINT ${name}", {name = name}))
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
rollback_to = function(tx, name)
|
rollback_to = function(tx, name)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
if name:is_blank() then error("Savepoint name cannot be empty") end
|
||||||
end
|
return tx.conn:exec("ROLLBACK TO SAVEPOINT {{name}}":parse({name = name}))
|
||||||
if string.is_blank(name) then
|
|
||||||
error("Savepoint name cannot be empty")
|
|
||||||
end
|
|
||||||
return tx.conn:exec(string.template("ROLLBACK TO SAVEPOINT ${name}", {name = name}))
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
query = function(tx, query_str, ...)
|
query = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:query(query_str, ...)
|
return tx.conn:query(query_str, ...)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
exec = function(tx, query_str, ...)
|
exec = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:exec(query_str, ...)
|
return tx.conn:exec(query_str, ...)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
query_row = function(tx, query_str, ...)
|
query_row = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:query_row(query_str, ...)
|
return tx.conn:query_row(query_str, ...)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
query_value = function(tx, query_str, ...)
|
query_value = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:query_value(query_str, ...)
|
return tx.conn:query_value(query_str, ...)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@ -131,38 +99,34 @@ function Connection:begin()
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified PostgreSQL parameter builder
|
-- Build PostgreSQL parameters ($1, $2, etc.)
|
||||||
local function build_postgres_params(data)
|
local function build_postgres_params(data)
|
||||||
local keys = table.keys(data)
|
local keys = table.keys(data)
|
||||||
local values = table.values(data)
|
local values = table.values(data)
|
||||||
local placeholders = {}
|
local placeholders = {}
|
||||||
|
|
||||||
for i = 1, #keys do
|
for i = 1, #keys do
|
||||||
table.insert(placeholders, string.template("$${num}", {num = tostring(i)}))
|
placeholders[i] = "$" .. i
|
||||||
end
|
end
|
||||||
|
|
||||||
return keys, values, placeholders, #keys
|
return keys, values, placeholders
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified query builders using table utilities
|
|
||||||
function Connection:insert(table_name, data, returning)
|
function Connection:insert(table_name, data, returning)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local keys, values, placeholders = build_postgres_params(data)
|
local keys, values, placeholders = build_postgres_params(data)
|
||||||
|
|
||||||
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
|
local query = "INSERT INTO {{table}} ({{columns}}) VALUES ({{placeholders}})":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
columns = table.concat(keys, ", "),
|
columns = keys:join(", "),
|
||||||
placeholders = table.concat(placeholders, ", ")
|
placeholders = table.concat(placeholders, ", ")
|
||||||
})
|
})
|
||||||
|
|
||||||
if returning and not string.is_blank(returning) then
|
if returning and not returning:is_blank() then
|
||||||
query = string.template("${query} RETURNING ${returning}", {
|
query = query .. " RETURNING " .. returning
|
||||||
query = query,
|
|
||||||
returning = returning
|
|
||||||
})
|
|
||||||
return self:query(query, unpack(values))
|
return self:query(query, unpack(values))
|
||||||
else
|
else
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
@ -170,37 +134,32 @@ function Connection:insert(table_name, data, returning)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Connection:upsert(table_name, data, conflict_columns, returning)
|
function Connection:upsert(table_name, data, conflict_columns, returning)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local keys, values, placeholders = build_postgres_params(data)
|
local keys, values, placeholders = build_postgres_params(data)
|
||||||
local updates = table.map(keys, function(key)
|
local updates = table.map(keys, function(key) return key .. " = EXCLUDED." .. key end)
|
||||||
return string.template("${key} = EXCLUDED.${key}", {key = key})
|
|
||||||
end)
|
|
||||||
|
|
||||||
local conflict_clause = ""
|
local conflict_clause = ""
|
||||||
if conflict_columns then
|
if conflict_columns then
|
||||||
if type(conflict_columns) == "string" then
|
if type(conflict_columns) == "string" then
|
||||||
conflict_clause = string.template("(${columns})", {columns = conflict_columns})
|
conflict_clause = "(" .. conflict_columns .. ")"
|
||||||
else
|
else
|
||||||
conflict_clause = string.template("(${columns})", {columns = table.concat(conflict_columns, ", ")})
|
conflict_clause = "(" .. table.concat(conflict_columns, ", ") .. ")"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT ${conflict} DO UPDATE SET ${updates}", {
|
local query = "INSERT INTO {{table}} ({{columns}}) VALUES ({{placeholders}}) ON CONFLICT {{conflict}} DO UPDATE SET {{updates}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
columns = table.concat(keys, ", "),
|
columns = keys:join(", "),
|
||||||
placeholders = table.concat(placeholders, ", "),
|
placeholders = table.concat(placeholders, ", "),
|
||||||
conflict = conflict_clause,
|
conflict = conflict_clause,
|
||||||
updates = table.concat(updates, ", ")
|
updates = updates:join(", ")
|
||||||
})
|
})
|
||||||
|
|
||||||
if returning and not string.is_blank(returning) then
|
if returning and not returning:is_blank() then
|
||||||
query = string.template("${query} RETURNING ${returning}", {
|
query = query .. " RETURNING " .. returning
|
||||||
query = query,
|
|
||||||
returning = returning
|
|
||||||
})
|
|
||||||
return self:query(query, unpack(values))
|
return self:query(query, unpack(values))
|
||||||
else
|
else
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
@ -208,10 +167,10 @@ function Connection:upsert(table_name, data, conflict_columns, returning)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Connection:update(table_name, data, where_clause, returning, ...)
|
function Connection:update(table_name, data, where_clause, returning, ...)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
if string.is_blank(where_clause) then
|
if where_clause:is_blank() then
|
||||||
error("WHERE clause cannot be empty for UPDATE")
|
error("WHERE clause cannot be empty for UPDATE")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -219,35 +178,29 @@ function Connection:update(table_name, data, where_clause, returning, ...)
|
|||||||
local values = table.values(data)
|
local values = table.values(data)
|
||||||
local param_count = #keys
|
local param_count = #keys
|
||||||
|
|
||||||
|
-- Build SET clause with numbered parameters
|
||||||
local sets = {}
|
local sets = {}
|
||||||
for i, key in ipairs(keys) do
|
for i, key in ipairs(keys) do
|
||||||
table.insert(sets, string.template("${key} = $${num}", {
|
sets[i] = key .. " = $" .. i
|
||||||
key = key,
|
|
||||||
num = tostring(i)
|
|
||||||
}))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle WHERE clause parameters
|
-- Handle WHERE parameters
|
||||||
local where_args = {...}
|
local where_args = {...}
|
||||||
local where_clause_with_params = where_clause
|
local where_clause_final = where_clause
|
||||||
for i = 1, #where_args do
|
for i = 1, #where_args do
|
||||||
param_count = param_count + 1
|
param_count = param_count + 1
|
||||||
table.insert(values, where_args[i])
|
values[#values + 1] = where_args[i]
|
||||||
where_clause_with_params = string.replace(where_clause_with_params, "?",
|
where_clause_final = where_clause_final:replace("?", "$" .. param_count, 1)
|
||||||
string.template("$${num}", {num = tostring(param_count)}), 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
|
local query = "UPDATE {{table}} SET {{sets}} WHERE {{where}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
sets = table.concat(sets, ", "),
|
sets = table.concat(sets, ", "),
|
||||||
where = where_clause_with_params
|
where = where_clause_final
|
||||||
})
|
})
|
||||||
|
|
||||||
if returning and not string.is_blank(returning) then
|
if returning and not returning:is_blank() then
|
||||||
query = string.template("${query} RETURNING ${returning}", {
|
query = query .. " RETURNING " .. returning
|
||||||
query = query,
|
|
||||||
returning = returning
|
|
||||||
})
|
|
||||||
return self:query(query, unpack(values))
|
return self:query(query, unpack(values))
|
||||||
else
|
else
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
@ -255,33 +208,29 @@ function Connection:update(table_name, data, where_clause, returning, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Connection:delete(table_name, where_clause, returning, ...)
|
function Connection:delete(table_name, where_clause, returning, ...)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
if string.is_blank(where_clause) then
|
if where_clause:is_blank() then
|
||||||
error("WHERE clause cannot be empty for DELETE")
|
error("WHERE clause cannot be empty for DELETE")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle WHERE clause parameters
|
|
||||||
local where_args = {...}
|
local where_args = {...}
|
||||||
local values = {}
|
local values = {}
|
||||||
local where_clause_with_params = where_clause
|
local where_clause_final = where_clause
|
||||||
|
|
||||||
for i = 1, #where_args do
|
for i = 1, #where_args do
|
||||||
table.insert(values, where_args[i])
|
values[i] = where_args[i]
|
||||||
where_clause_with_params = string.replace(where_clause_with_params, "?",
|
where_clause_final = where_clause_final:replace("?", "$" .. i, 1)
|
||||||
string.template("$${num}", {num = tostring(i)}), 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("DELETE FROM ${table} WHERE ${where}", {
|
local query = "DELETE FROM {{table}} WHERE {{where}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
where = where_clause_with_params
|
where = where_clause_final
|
||||||
})
|
})
|
||||||
|
|
||||||
if returning and not string.is_blank(returning) then
|
if returning and not returning:is_blank() then
|
||||||
query = string.template("${query} RETURNING ${returning}", {
|
query = query .. " RETURNING " .. returning
|
||||||
query = query,
|
|
||||||
returning = returning
|
|
||||||
})
|
|
||||||
return self:query(query, unpack(values))
|
return self:query(query, unpack(values))
|
||||||
else
|
else
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
@ -289,7 +238,7 @@ function Connection:delete(table_name, where_clause, returning, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Connection:select(table_name, columns, where_clause, ...)
|
function Connection:select(table_name, columns, where_clause, ...)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -298,26 +247,24 @@ function Connection:select(table_name, columns, where_clause, ...)
|
|||||||
columns = table.concat(columns, ", ")
|
columns = table.concat(columns, ", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
local query
|
if where_clause and not where_clause:is_blank() then
|
||||||
if where_clause and not string.is_blank(where_clause) then
|
|
||||||
-- Handle WHERE clause parameters
|
|
||||||
local where_args = {...}
|
local where_args = {...}
|
||||||
local values = {}
|
local values = {}
|
||||||
local where_clause_with_params = where_clause
|
local where_clause_final = where_clause
|
||||||
|
|
||||||
for i = 1, #where_args do
|
for i = 1, #where_args do
|
||||||
table.insert(values, where_args[i])
|
values[i] = where_args[i]
|
||||||
where_clause_with_params = string.replace(where_clause_with_params, "?",
|
where_clause_final = where_clause_final:replace("?", "$" .. i, 1)
|
||||||
string.template("$${num}", {num = tostring(i)}), 1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
|
local query = "SELECT {{columns}} FROM {{table}} WHERE {{where}}":parse({
|
||||||
columns = columns,
|
columns = columns,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
where = where_clause_with_params
|
where = where_clause_final
|
||||||
})
|
})
|
||||||
return self:query(query, unpack(values))
|
return self:query(query, unpack(values))
|
||||||
else
|
else
|
||||||
query = string.template("SELECT ${columns} FROM ${table}", {
|
local query = "SELECT {{columns}} FROM {{table}}":parse({
|
||||||
columns = columns,
|
columns = columns,
|
||||||
table = table_name
|
table = table_name
|
||||||
})
|
})
|
||||||
@ -325,163 +272,137 @@ function Connection:select(table_name, columns, where_clause, ...)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Enhanced PostgreSQL schema helpers
|
-- Schema helpers
|
||||||
function Connection:table_exists(table_name, schema_name)
|
function Connection:table_exists(table_name, schema_name)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then return false end
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
schema_name = schema_name or "public"
|
schema_name = schema_name or "public"
|
||||||
local result = self:query_value(
|
return self:query_value("SELECT tablename FROM pg_tables WHERE schemaname = $1 AND tablename = $2",
|
||||||
"SELECT tablename FROM pg_tables WHERE schemaname = $1 AND tablename = $2",
|
schema_name:trim(), table_name:trim()) ~= nil
|
||||||
string.trim(schema_name), string.trim(table_name)
|
|
||||||
)
|
|
||||||
return result ~= nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:column_exists(table_name, column_name, schema_name)
|
function Connection:column_exists(table_name, column_name, schema_name)
|
||||||
if string.is_blank(table_name) or string.is_blank(column_name) then
|
if table_name:is_blank() or column_name:is_blank() then return false end
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
schema_name = schema_name or "public"
|
schema_name = schema_name or "public"
|
||||||
local result = self:query_value([[
|
return self:query_value([[
|
||||||
SELECT column_name FROM information_schema.columns
|
SELECT column_name FROM information_schema.columns
|
||||||
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
|
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
|
||||||
]], string.trim(schema_name), string.trim(table_name), string.trim(column_name))
|
]], schema_name:trim(), table_name:trim(), column_name:trim()) ~= nil
|
||||||
return result ~= nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:create_table(table_name, schema)
|
function Connection:create_table(table_name, schema)
|
||||||
if string.is_blank(table_name) or string.is_blank(schema) then
|
if table_name:is_blank() or schema:is_blank() then
|
||||||
error("Table name and schema cannot be empty")
|
error("Table name and schema cannot be empty")
|
||||||
end
|
end
|
||||||
|
return self:exec("CREATE TABLE IF NOT EXISTS {{table}} ({{schema}})":parse({
|
||||||
local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", {
|
|
||||||
table = table_name,
|
table = table_name,
|
||||||
schema = string.trim(schema)
|
schema = schema:trim()
|
||||||
})
|
}))
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:drop_table(table_name, cascade)
|
function Connection:drop_table(table_name, cascade)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local cascade_clause = cascade and " CASCADE" or ""
|
local cascade_clause = cascade and " CASCADE" or ""
|
||||||
local query = string.template("DROP TABLE IF EXISTS ${table}${cascade}", {
|
return self:exec("DROP TABLE IF EXISTS {{table}}{{cascade}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
cascade = cascade_clause
|
cascade = cascade_clause
|
||||||
})
|
}))
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:add_column(table_name, column_def)
|
function Connection:add_column(table_name, column_def)
|
||||||
if string.is_blank(table_name) or string.is_blank(column_def) then
|
if table_name:is_blank() or column_def:is_blank() then
|
||||||
error("Table name and column definition cannot be empty")
|
error("Table name and column definition cannot be empty")
|
||||||
end
|
end
|
||||||
|
return self:exec("ALTER TABLE {{table}} ADD COLUMN IF NOT EXISTS {{column}}":parse({
|
||||||
local query = string.template("ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column}", {
|
|
||||||
table = table_name,
|
table = table_name,
|
||||||
column = string.trim(column_def)
|
column = column_def:trim()
|
||||||
})
|
}))
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:drop_column(table_name, column_name, cascade)
|
function Connection:drop_column(table_name, column_name, cascade)
|
||||||
if string.is_blank(table_name) or string.is_blank(column_name) then
|
if table_name:is_blank() or column_name:is_blank() then
|
||||||
error("Table name and column name cannot be empty")
|
error("Table name and column name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local cascade_clause = cascade and " CASCADE" or ""
|
local cascade_clause = cascade and " CASCADE" or ""
|
||||||
local query = string.template("ALTER TABLE ${table} DROP COLUMN IF EXISTS ${column}${cascade}", {
|
return self:exec("ALTER TABLE {{table}} DROP COLUMN IF EXISTS {{column}}{{cascade}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
column = column_name,
|
column = column_name,
|
||||||
cascade = cascade_clause
|
cascade = cascade_clause
|
||||||
})
|
}))
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:create_index(index_name, table_name, columns, unique, method)
|
function Connection:create_index(index_name, table_name, columns, unique, method)
|
||||||
if string.is_blank(index_name) or string.is_blank(table_name) then
|
if index_name:is_blank() or table_name:is_blank() then
|
||||||
error("Index name and table name cannot be empty")
|
error("Index name and table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local unique_clause = unique and "UNIQUE " or ""
|
local unique_clause = unique and "UNIQUE " or ""
|
||||||
local method_clause = method and string.template(" USING ${method}", {method = string.upper(method)}) or ""
|
local method_clause = method and " USING " .. method:upper() or ""
|
||||||
local columns_str = type(columns) == "table" and table.concat(columns, ", ") or tostring(columns)
|
local columns_str = type(columns) == "table" and table.concat(columns, ", ") or tostring(columns)
|
||||||
|
|
||||||
local query = string.template("CREATE ${unique}INDEX IF NOT EXISTS ${index} ON ${table}${method} (${columns})", {
|
return self:exec("CREATE {{unique}}INDEX IF NOT EXISTS {{index}} ON {{table}}{{method}} ({{columns}})":parse({
|
||||||
unique = unique_clause,
|
unique = unique_clause,
|
||||||
index = index_name,
|
index = index_name,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
method = method_clause,
|
method = method_clause,
|
||||||
columns = columns_str
|
columns = columns_str
|
||||||
})
|
}))
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:drop_index(index_name, cascade)
|
function Connection:drop_index(index_name, cascade)
|
||||||
if string.is_blank(index_name) then
|
if index_name:is_blank() then
|
||||||
error("Index name cannot be empty")
|
error("Index name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local cascade_clause = cascade and " CASCADE" or ""
|
local cascade_clause = cascade and " CASCADE" or ""
|
||||||
local query = string.template("DROP INDEX IF EXISTS ${index}${cascade}", {
|
return self:exec("DROP INDEX IF EXISTS {{index}}{{cascade}}":parse({
|
||||||
index = index_name,
|
index = index_name,
|
||||||
cascade = cascade_clause
|
cascade = cascade_clause
|
||||||
})
|
}))
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PostgreSQL-specific functions
|
-- PostgreSQL-specific functions
|
||||||
function Connection:vacuum(table_name, analyze)
|
function Connection:vacuum(table_name, analyze)
|
||||||
local analyze_clause = analyze and " ANALYZE" or ""
|
local analyze_clause = analyze and " ANALYZE" or ""
|
||||||
local table_clause = table_name and string.template(" ${table}", {table = table_name}) or ""
|
local table_clause = table_name and " " .. table_name or ""
|
||||||
return self:exec(string.template("VACUUM${analyze}${table}", {
|
return self:exec("VACUUM{{analyze}}{{table}}":parse({
|
||||||
analyze = analyze_clause,
|
analyze = analyze_clause,
|
||||||
table = table_clause
|
table = table_clause
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:analyze(table_name)
|
function Connection:analyze(table_name)
|
||||||
local table_clause = table_name and string.template(" ${table}", {table = table_name}) or ""
|
local table_clause = table_name and " " .. table_name or ""
|
||||||
return self:exec(string.template("ANALYZE${table}", {table = table_clause}))
|
return self:exec("ANALYZE{{table}}":parse({table = table_clause}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:reindex(name, type)
|
function Connection:reindex(name, type)
|
||||||
if string.is_blank(name) then
|
if name:is_blank() then
|
||||||
error("Name cannot be empty for REINDEX")
|
error("Name cannot be empty for REINDEX")
|
||||||
end
|
end
|
||||||
|
|
||||||
type = type or "INDEX"
|
type = (type or "INDEX"):upper()
|
||||||
local valid_types = {"INDEX", "TABLE", "SCHEMA", "DATABASE", "SYSTEM"}
|
local valid_types = {"INDEX", "TABLE", "SCHEMA", "DATABASE", "SYSTEM"}
|
||||||
local type_upper = string.upper(type)
|
|
||||||
|
|
||||||
if not table.contains(valid_types, type_upper) then
|
if not table.contains(valid_types, type) then
|
||||||
error(string.template("Invalid REINDEX type: ${type}", {type = type}))
|
error("Invalid REINDEX type: " .. type)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self:exec(string.template("REINDEX ${type} ${name}", {
|
return self:exec("REINDEX {{type}} {{name}}":parse({type = type, name = name}))
|
||||||
type = type_upper,
|
|
||||||
name = name
|
|
||||||
}))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- PostgreSQL settings and introspection
|
|
||||||
function Connection:show(setting)
|
function Connection:show(setting)
|
||||||
if string.is_blank(setting) then
|
if setting:is_blank() then
|
||||||
error("Setting name cannot be empty")
|
error("Setting name cannot be empty")
|
||||||
end
|
end
|
||||||
return self:query_value(string.template("SHOW ${setting}", {setting = setting}))
|
return self:query_value("SHOW {{setting}}":parse({setting = setting}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:set(setting, value)
|
function Connection:set(setting, value)
|
||||||
if string.is_blank(setting) then
|
if setting:is_blank() then
|
||||||
error("Setting name cannot be empty")
|
error("Setting name cannot be empty")
|
||||||
end
|
end
|
||||||
return self:exec(string.template("SET ${setting} = ${value}", {
|
return self:exec("SET {{setting}} = {{value}}":parse({
|
||||||
setting = setting,
|
setting = setting,
|
||||||
value = tostring(value)
|
value = tostring(value)
|
||||||
}))
|
}))
|
||||||
@ -505,12 +426,11 @@ end
|
|||||||
|
|
||||||
function Connection:list_tables(schema_name)
|
function Connection:list_tables(schema_name)
|
||||||
schema_name = schema_name or "public"
|
schema_name = schema_name or "public"
|
||||||
return self:query("SELECT tablename FROM pg_tables WHERE schemaname = $1 ORDER BY tablename",
|
return self:query("SELECT tablename FROM pg_tables WHERE schemaname = $1 ORDER BY tablename", schema_name:trim())
|
||||||
string.trim(schema_name))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:describe_table(table_name, schema_name)
|
function Connection:describe_table(table_name, schema_name)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -520,64 +440,64 @@ function Connection:describe_table(table_name, schema_name)
|
|||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_schema = $1 AND table_name = $2
|
WHERE table_schema = $1 AND table_name = $2
|
||||||
ORDER BY ordinal_position
|
ORDER BY ordinal_position
|
||||||
]], string.trim(schema_name), string.trim(table_name))
|
]], schema_name:trim(), table_name:trim())
|
||||||
end
|
end
|
||||||
|
|
||||||
-- JSON/JSONB helpers
|
-- JSON/JSONB helpers
|
||||||
function Connection:json_extract(column, path)
|
function Connection:json_extract(column, path)
|
||||||
if string.is_blank(column) or string.is_blank(path) then
|
if column:is_blank() or path:is_blank() then
|
||||||
error("Column and path cannot be empty")
|
error("Column and path cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template("${column}->'${path}'", {column = column, path = path})
|
return "{{column}}->'{{path}}'":parse({column = column, path = path})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:json_extract_text(column, path)
|
function Connection:json_extract_text(column, path)
|
||||||
if string.is_blank(column) or string.is_blank(path) then
|
if column:is_blank() or path:is_blank() then
|
||||||
error("Column and path cannot be empty")
|
error("Column and path cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template("${column}->>'${path}'", {column = column, path = path})
|
return "{{column}}->>'{{path}}'":parse({column = column, path = path})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:jsonb_contains(column, value)
|
function Connection:jsonb_contains(column, value)
|
||||||
if string.is_blank(column) or string.is_blank(value) then
|
if column:is_blank() or value:is_blank() then
|
||||||
error("Column and value cannot be empty")
|
error("Column and value cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template("${column} @> '${value}'", {column = column, value = value})
|
return "{{column}} @> '{{value}}'":parse({column = column, value = value})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:jsonb_contained_by(column, value)
|
function Connection:jsonb_contained_by(column, value)
|
||||||
if string.is_blank(column) or string.is_blank(value) then
|
if column:is_blank() or value:is_blank() then
|
||||||
error("Column and value cannot be empty")
|
error("Column and value cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template("${column} <@ '${value}'", {column = column, value = value})
|
return "{{column}} <@ '{{value}}'":parse({column = column, value = value})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Array helpers
|
-- Array helpers
|
||||||
function Connection:array_contains(column, value)
|
function Connection:array_contains(column, value)
|
||||||
if string.is_blank(column) then
|
if column:is_blank() then
|
||||||
error("Column cannot be empty")
|
error("Column cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template("$1 = ANY(${column})", {column = column})
|
return "$1 = ANY({{column}})":parse({column = column})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:array_length(column)
|
function Connection:array_length(column)
|
||||||
if string.is_blank(column) then
|
if column:is_blank() then
|
||||||
error("Column cannot be empty")
|
error("Column cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template("array_length(${column}, 1)", {column = column})
|
return "array_length({{column}}, 1)":parse({column = column})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Connection management
|
-- Connection management
|
||||||
function postgres.parse_dsn(dsn)
|
function postgres.parse_dsn(dsn)
|
||||||
if string.is_blank(dsn) then
|
if dsn:is_blank() then
|
||||||
return nil, "DSN cannot be empty"
|
return nil, "DSN cannot be empty"
|
||||||
end
|
end
|
||||||
|
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for pair in string.trim(dsn):gmatch("[^%s]+") do
|
for pair in dsn:trim():gmatch("[^%s]+") do
|
||||||
local key, value = pair:match("([^=]+)=(.+)")
|
local key, value = pair:match("([^=]+)=(.+)")
|
||||||
if key and value then
|
if key and value then
|
||||||
parts[string.trim(key)] = string.trim(value)
|
parts[key:trim()] = value:trim()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -585,15 +505,13 @@ function postgres.parse_dsn(dsn)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function postgres.connect(dsn)
|
function postgres.connect(dsn)
|
||||||
if string.is_blank(dsn) then
|
if dsn:is_blank() then
|
||||||
error("DSN cannot be empty")
|
error("DSN cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local conn_id = moonshark.sql_connect("postgres", string.trim(dsn))
|
local conn_id = moonshark.sql_connect("postgres", dsn:trim())
|
||||||
if conn_id then
|
if conn_id then
|
||||||
local conn = {_id = conn_id}
|
return setmetatable({_id = conn_id}, Connection)
|
||||||
setmetatable(conn, Connection)
|
|
||||||
return conn
|
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@ -625,10 +543,7 @@ end
|
|||||||
|
|
||||||
function postgres.query_row(dsn, query_str, ...)
|
function postgres.query_row(dsn, query_str, ...)
|
||||||
local results = postgres.query(dsn, query_str, ...)
|
local results = postgres.query(dsn, query_str, ...)
|
||||||
if results and #results > 0 then
|
return results and #results > 0 and results[1] or nil
|
||||||
return results[1]
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function postgres.query_value(dsn, query_str, ...)
|
function postgres.query_value(dsn, query_str, ...)
|
||||||
@ -649,8 +564,7 @@ function postgres.migrate(dsn, migrations, schema)
|
|||||||
error("Failed to connect to PostgreSQL database for migration")
|
error("Failed to connect to PostgreSQL database for migration")
|
||||||
end
|
end
|
||||||
|
|
||||||
conn:create_table("_migrations",
|
conn:create_table("_migrations", "id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW()")
|
||||||
"id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW()")
|
|
||||||
|
|
||||||
local tx = conn:begin()
|
local tx = conn:begin()
|
||||||
if not tx then
|
if not tx then
|
||||||
@ -658,18 +572,14 @@ function postgres.migrate(dsn, migrations, schema)
|
|||||||
error("Failed to begin migration transaction")
|
error("Failed to begin migration transaction")
|
||||||
end
|
end
|
||||||
|
|
||||||
local success = true
|
|
||||||
local error_msg = ""
|
|
||||||
|
|
||||||
for _, migration in ipairs(migrations) do
|
for _, migration in ipairs(migrations) do
|
||||||
if not migration.name or string.is_blank(migration.name) then
|
if not migration.name or migration.name:is_blank() then
|
||||||
error_msg = "Migration must have a non-empty name"
|
tx:rollback()
|
||||||
success = false
|
conn:close()
|
||||||
break
|
error("Migration must have a non-empty name")
|
||||||
end
|
end
|
||||||
|
|
||||||
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = $1",
|
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = $1", migration.name:trim())
|
||||||
string.trim(migration.name))
|
|
||||||
if not existing then
|
if not existing then
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
if type(migration.up) == "string" then
|
if type(migration.up) == "string" then
|
||||||
@ -682,52 +592,34 @@ function postgres.migrate(dsn, migrations, schema)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
conn:exec("INSERT INTO _migrations (name) VALUES ($1)", string.trim(migration.name))
|
conn:exec("INSERT INTO _migrations (name) VALUES ($1)", migration.name:trim())
|
||||||
print(string.template("Applied migration: ${name}", {name = migration.name}))
|
print("Applied migration: {{name}}":parse({name = migration.name}))
|
||||||
else
|
|
||||||
success = false
|
|
||||||
error_msg = string.template("Migration '${name}' failed: ${error}", {
|
|
||||||
name = migration.name,
|
|
||||||
error = err or "unknown error"
|
|
||||||
})
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if success then
|
|
||||||
tx:commit()
|
|
||||||
else
|
else
|
||||||
tx:rollback()
|
tx:rollback()
|
||||||
conn:close()
|
conn:close()
|
||||||
error(error_msg)
|
error("Migration '{{name}}' failed: {{error}}":parse({
|
||||||
|
name = migration.name,
|
||||||
|
error = err or "unknown error"
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tx:commit()
|
||||||
conn:close()
|
conn:close()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified result processing utilities
|
-- Result processing utilities
|
||||||
function postgres.to_array(results, column_name)
|
function postgres.to_array(results, column_name)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then return {} end
|
||||||
return {}
|
if column_name:is_blank() then error("Column name cannot be empty") end
|
||||||
end
|
|
||||||
|
|
||||||
if string.is_blank(column_name) then
|
|
||||||
error("Column name cannot be empty")
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.map(results, function(row) return row[column_name] end)
|
return table.map(results, function(row) return row[column_name] end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function postgres.to_map(results, key_column, value_column)
|
function postgres.to_map(results, key_column, value_column)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then return {} end
|
||||||
return {}
|
if key_column:is_blank() then error("Key column name cannot be empty") end
|
||||||
end
|
|
||||||
|
|
||||||
if string.is_blank(key_column) then
|
|
||||||
error("Key column name cannot be empty")
|
|
||||||
end
|
|
||||||
|
|
||||||
local map = {}
|
local map = {}
|
||||||
for _, row in ipairs(results) do
|
for _, row in ipairs(results) do
|
||||||
@ -738,18 +630,11 @@ function postgres.to_map(results, key_column, value_column)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function postgres.group_by(results, column_name)
|
function postgres.group_by(results, column_name)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then return {} end
|
||||||
return {}
|
if column_name:is_blank() then error("Column name cannot be empty") end
|
||||||
end
|
|
||||||
|
|
||||||
if string.is_blank(column_name) then
|
|
||||||
error("Column name cannot be empty")
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.group_by(results, function(row) return row[column_name] end)
|
return table.group_by(results, function(row) return row[column_name] end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified debug helper
|
|
||||||
function postgres.print_results(results)
|
function postgres.print_results(results)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then
|
||||||
print("No results")
|
print("No results")
|
||||||
@ -762,18 +647,15 @@ function postgres.print_results(results)
|
|||||||
-- Calculate column widths
|
-- Calculate column widths
|
||||||
local widths = {}
|
local widths = {}
|
||||||
for _, col in ipairs(columns) do
|
for _, col in ipairs(columns) do
|
||||||
widths[col] = string.length(col)
|
widths[col] = col:length()
|
||||||
end
|
|
||||||
|
|
||||||
for _, row in ipairs(results) do
|
for _, row in ipairs(results) do
|
||||||
for _, col in ipairs(columns) do
|
|
||||||
local value = tostring(row[col] or "")
|
local value = tostring(row[col] or "")
|
||||||
widths[col] = math.max(widths[col], string.length(value))
|
widths[col] = math.max(widths[col], value:length())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Print header
|
-- Print header and separator
|
||||||
local header_parts = table.map(columns, function(col) return string.pad_right(col, widths[col]) end)
|
local header_parts = table.map(columns, function(col) return col:pad_right(widths[col]) end)
|
||||||
local separator_parts = table.map(columns, function(col) return string.repeat_("-", widths[col]) end)
|
local separator_parts = table.map(columns, function(col) return string.repeat_("-", widths[col]) end)
|
||||||
|
|
||||||
print(table.concat(header_parts, " | "))
|
print(table.concat(header_parts, " | "))
|
||||||
@ -783,22 +665,22 @@ function postgres.print_results(results)
|
|||||||
for _, row in ipairs(results) do
|
for _, row in ipairs(results) do
|
||||||
local value_parts = table.map(columns, function(col)
|
local value_parts = table.map(columns, function(col)
|
||||||
local value = tostring(row[col] or "")
|
local value = tostring(row[col] or "")
|
||||||
return string.pad_right(value, widths[col])
|
return value:pad_right(widths[col])
|
||||||
end)
|
end)
|
||||||
print(table.concat(value_parts, " | "))
|
print(table.concat(value_parts, " | "))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function postgres.escape_identifier(name)
|
function postgres.escape_identifier(name)
|
||||||
if string.is_blank(name) then
|
if name:is_blank() then
|
||||||
error("Identifier name cannot be empty")
|
error("Identifier name cannot be empty")
|
||||||
end
|
end
|
||||||
return string.template('"${name}"', {name = string.replace(name, '"', '""')})
|
return '"{{name}}"':parse({name = name:replace('"', '""')})
|
||||||
end
|
end
|
||||||
|
|
||||||
function postgres.escape_literal(value)
|
function postgres.escape_literal(value)
|
||||||
if type(value) == "string" then
|
if type(value) == "string" then
|
||||||
return string.template("'${value}'", {value = string.replace(value, "'", "''")})
|
return "'{{value}}'":parse({value = value:replace("'", "''")})
|
||||||
end
|
end
|
||||||
return tostring(value)
|
return tostring(value)
|
||||||
end
|
end
|
||||||
|
@ -23,24 +23,19 @@ function Connection:query(query_str, ...)
|
|||||||
if not self._id then
|
if not self._id then
|
||||||
error("Connection is closed")
|
error("Connection is closed")
|
||||||
end
|
end
|
||||||
query_str = string.normalize_whitespace(query_str)
|
return moonshark.sql_query(self._id, query_str:normalize_whitespace(), ...)
|
||||||
return moonshark.sql_query(self._id, query_str, ...)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:exec(query_str, ...)
|
function Connection:exec(query_str, ...)
|
||||||
if not self._id then
|
if not self._id then
|
||||||
error("Connection is closed")
|
error("Connection is closed")
|
||||||
end
|
end
|
||||||
query_str = string.normalize_whitespace(query_str)
|
return moonshark.sql_exec(self._id, query_str:normalize_whitespace(), ...)
|
||||||
return moonshark.sql_exec(self._id, query_str, ...)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:query_row(query_str, ...)
|
function Connection:query_row(query_str, ...)
|
||||||
local results = self:query(query_str, ...)
|
local results = self:query(query_str, ...)
|
||||||
if results and #results > 0 then
|
return results and #results > 0 and results[1] or nil
|
||||||
return results[1]
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:query_value(query_str, ...)
|
function Connection:query_value(query_str, ...)
|
||||||
@ -53,57 +48,40 @@ function Connection:query_value(query_str, ...)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Enhanced transaction support
|
|
||||||
function Connection:begin()
|
function Connection:begin()
|
||||||
local result = self:exec("BEGIN")
|
local result = self:exec("BEGIN")
|
||||||
if result then
|
if result then
|
||||||
return {
|
return {
|
||||||
conn = self,
|
conn = self,
|
||||||
active = true,
|
active = true,
|
||||||
|
|
||||||
commit = function(tx)
|
commit = function(tx)
|
||||||
if tx.active then
|
if tx.active then
|
||||||
local result = tx.conn:exec("COMMIT")
|
|
||||||
tx.active = false
|
tx.active = false
|
||||||
return result
|
return tx.conn:exec("COMMIT")
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
|
|
||||||
rollback = function(tx)
|
rollback = function(tx)
|
||||||
if tx.active then
|
if tx.active then
|
||||||
local result = tx.conn:exec("ROLLBACK")
|
|
||||||
tx.active = false
|
tx.active = false
|
||||||
return result
|
return tx.conn:exec("ROLLBACK")
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
|
|
||||||
query = function(tx, query_str, ...)
|
query = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:query(query_str, ...)
|
return tx.conn:query(query_str, ...)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
exec = function(tx, query_str, ...)
|
exec = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:exec(query_str, ...)
|
return tx.conn:exec(query_str, ...)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
query_row = function(tx, query_str, ...)
|
query_row = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:query_row(query_str, ...)
|
return tx.conn:query_row(query_str, ...)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
query_value = function(tx, query_str, ...)
|
query_value = function(tx, query_str, ...)
|
||||||
if not tx.active then
|
if not tx.active then error("Transaction is not active") end
|
||||||
error("Transaction is not active")
|
|
||||||
end
|
|
||||||
return tx.conn:query_value(query_str, ...)
|
return tx.conn:query_value(query_str, ...)
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@ -111,93 +89,85 @@ function Connection:begin()
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified query builders using table utilities
|
|
||||||
function Connection:insert(table_name, data)
|
function Connection:insert(table_name, data)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local keys = table.keys(data)
|
local keys = table.keys(data)
|
||||||
local values = table.values(data)
|
local values = table.values(data)
|
||||||
local placeholders = table.map(keys, function() return "?" end)
|
local placeholders = string.repeat_("?, ", #keys):trim_right(", ")
|
||||||
|
|
||||||
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
|
local query = "INSERT INTO {{table}} ({{columns}}) VALUES ({{placeholders}})":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
columns = table.concat(keys, ", "),
|
columns = keys:join(", "),
|
||||||
placeholders = table.concat(placeholders, ", ")
|
placeholders = placeholders
|
||||||
})
|
})
|
||||||
|
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:upsert(table_name, data, conflict_columns)
|
function Connection:upsert(table_name, data, conflict_columns)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local keys = table.keys(data)
|
local keys = table.keys(data)
|
||||||
local values = table.values(data)
|
local values = table.values(data)
|
||||||
local placeholders = table.map(keys, function() return "?" end)
|
local placeholders = string.repeat_("?, ", #keys):trim_right(", ")
|
||||||
local updates = table.map(keys, function(key)
|
local updates = table.map(keys, function(key) return key .. " = excluded." .. key end):join(", ")
|
||||||
return string.template("${key} = excluded.${key}", {key = key})
|
|
||||||
end)
|
|
||||||
|
|
||||||
local conflict_clause = ""
|
local conflict_clause = ""
|
||||||
if conflict_columns then
|
if conflict_columns then
|
||||||
if type(conflict_columns) == "string" then
|
if type(conflict_columns) == "string" then
|
||||||
conflict_clause = string.template("(${columns})", {columns = conflict_columns})
|
conflict_clause = "(" .. conflict_columns .. ")"
|
||||||
else
|
else
|
||||||
conflict_clause = string.template("(${columns})", {columns = table.concat(conflict_columns, ", ")})
|
conflict_clause = "(" .. table.concat(conflict_columns, ", ") .. ")"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT ${conflict} DO UPDATE SET ${updates}", {
|
local query = "INSERT INTO {{table}} ({{columns}}) VALUES ({{placeholders}}) ON CONFLICT {{conflict}} DO UPDATE SET {{updates}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
columns = table.concat(keys, ", "),
|
columns = keys:join(", "),
|
||||||
placeholders = table.concat(placeholders, ", "),
|
placeholders = placeholders,
|
||||||
conflict = conflict_clause,
|
conflict = conflict_clause,
|
||||||
updates = table.concat(updates, ", ")
|
updates = updates
|
||||||
})
|
})
|
||||||
|
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:update(table_name, data, where_clause, ...)
|
function Connection:update(table_name, data, where_clause, ...)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
if string.is_blank(where_clause) then
|
if where_clause:is_blank() then
|
||||||
error("WHERE clause cannot be empty for UPDATE")
|
error("WHERE clause cannot be empty for UPDATE")
|
||||||
end
|
end
|
||||||
|
|
||||||
local keys = table.keys(data)
|
local keys = table.keys(data)
|
||||||
local values = table.values(data)
|
local values = table.values(data)
|
||||||
local sets = table.map(keys, function(key)
|
local sets = table.map(keys, function(key) return key .. " = ?" end):join(", ")
|
||||||
return string.template("${key} = ?", {key = key})
|
|
||||||
end)
|
|
||||||
|
|
||||||
local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
|
local query = "UPDATE {{table}} SET {{sets}} WHERE {{where}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
sets = table.concat(sets, ", "),
|
sets = sets,
|
||||||
where = where_clause
|
where = where_clause
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Add WHERE clause parameters
|
table.extend(values, {...})
|
||||||
local where_args = {...}
|
|
||||||
table.extend(values, where_args)
|
|
||||||
|
|
||||||
return self:exec(query, unpack(values))
|
return self:exec(query, unpack(values))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:delete(table_name, where_clause, ...)
|
function Connection:delete(table_name, where_clause, ...)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
if string.is_blank(where_clause) then
|
if where_clause:is_blank() then
|
||||||
error("WHERE clause cannot be empty for DELETE")
|
error("WHERE clause cannot be empty for DELETE")
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("DELETE FROM ${table} WHERE ${where}", {
|
local query = "DELETE FROM {{table}} WHERE {{where}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
where = where_clause
|
where = where_clause
|
||||||
})
|
})
|
||||||
@ -205,7 +175,7 @@ function Connection:delete(table_name, where_clause, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Connection:select(table_name, columns, where_clause, ...)
|
function Connection:select(table_name, columns, where_clause, ...)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -214,16 +184,15 @@ function Connection:select(table_name, columns, where_clause, ...)
|
|||||||
columns = table.concat(columns, ", ")
|
columns = table.concat(columns, ", ")
|
||||||
end
|
end
|
||||||
|
|
||||||
local query
|
if where_clause and not where_clause:is_blank() then
|
||||||
if where_clause and not string.is_blank(where_clause) then
|
local query = "SELECT {{columns}} FROM {{table}} WHERE {{where}}":parse({
|
||||||
query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
|
|
||||||
columns = columns,
|
columns = columns,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
where = where_clause
|
where = where_clause
|
||||||
})
|
})
|
||||||
return self:query(query, ...)
|
return self:query(query, ...)
|
||||||
else
|
else
|
||||||
query = string.template("SELECT ${columns} FROM ${table}", {
|
local query = "SELECT {{columns}} FROM {{table}}":parse({
|
||||||
columns = columns,
|
columns = columns,
|
||||||
table = table_name
|
table = table_name
|
||||||
})
|
})
|
||||||
@ -231,75 +200,63 @@ function Connection:select(table_name, columns, where_clause, ...)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Schema helpers
|
|
||||||
function Connection:table_exists(table_name)
|
function Connection:table_exists(table_name)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then return false end
|
||||||
return false
|
return self:query_value("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table_name:trim()) ~= nil
|
||||||
end
|
|
||||||
|
|
||||||
local result = self:query_value(
|
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
|
||||||
string.trim(table_name)
|
|
||||||
)
|
|
||||||
return result ~= nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:column_exists(table_name, column_name)
|
function Connection:column_exists(table_name, column_name)
|
||||||
if string.is_blank(table_name) or string.is_blank(column_name) then
|
if table_name:is_blank() or column_name:is_blank() then return false end
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = self:query(string.template("PRAGMA table_info(${table})", {table = table_name}))
|
local result = self:query("PRAGMA table_info({{table}})":parse({table = table_name}))
|
||||||
if result then
|
if result then
|
||||||
return table.any(result, function(row)
|
return table.any(result, function(row)
|
||||||
return string.iequals(row.name, string.trim(column_name))
|
return row.name:iequals(column_name:trim())
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:create_table(table_name, schema)
|
function Connection:create_table(table_name, schema)
|
||||||
if string.is_blank(table_name) or string.is_blank(schema) then
|
if table_name:is_blank() or schema:is_blank() then
|
||||||
error("Table name and schema cannot be empty")
|
error("Table name and schema cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", {
|
local query = "CREATE TABLE IF NOT EXISTS {{table}} ({{schema}})":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
schema = string.trim(schema)
|
schema = schema:trim()
|
||||||
})
|
})
|
||||||
return self:exec(query)
|
return self:exec(query)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:drop_table(table_name)
|
function Connection:drop_table(table_name)
|
||||||
if string.is_blank(table_name) then
|
if table_name:is_blank() then
|
||||||
error("Table name cannot be empty")
|
error("Table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
return self:exec("DROP TABLE IF EXISTS {{table}}":parse({table = table_name}))
|
||||||
local query = string.template("DROP TABLE IF EXISTS ${table}", {table = table_name})
|
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:add_column(table_name, column_def)
|
function Connection:add_column(table_name, column_def)
|
||||||
if string.is_blank(table_name) or string.is_blank(column_def) then
|
if table_name:is_blank() or column_def:is_blank() then
|
||||||
error("Table name and column definition cannot be empty")
|
error("Table name and column definition cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local query = string.template("ALTER TABLE ${table} ADD COLUMN ${column}", {
|
local query = "ALTER TABLE {{table}} ADD COLUMN {{column}}":parse({
|
||||||
table = table_name,
|
table = table_name,
|
||||||
column = string.trim(column_def)
|
column = column_def:trim()
|
||||||
})
|
})
|
||||||
return self:exec(query)
|
return self:exec(query)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:create_index(index_name, table_name, columns, unique)
|
function Connection:create_index(index_name, table_name, columns, unique)
|
||||||
if string.is_blank(index_name) or string.is_blank(table_name) then
|
if index_name:is_blank() or table_name:is_blank() then
|
||||||
error("Index name and table name cannot be empty")
|
error("Index name and table name cannot be empty")
|
||||||
end
|
end
|
||||||
|
|
||||||
local unique_clause = unique and "UNIQUE " or ""
|
local unique_clause = unique and "UNIQUE " or ""
|
||||||
local columns_str = type(columns) == "table" and table.concat(columns, ", ") or tostring(columns)
|
local columns_str = type(columns) == "table" and table.concat(columns, ", ") or tostring(columns)
|
||||||
|
|
||||||
local query = string.template("CREATE ${unique}INDEX IF NOT EXISTS ${index} ON ${table} (${columns})", {
|
local query = "CREATE {{unique}}INDEX IF NOT EXISTS {{index}} ON {{table}} ({{columns}})":parse({
|
||||||
unique = unique_clause,
|
unique = unique_clause,
|
||||||
index = index_name,
|
index = index_name,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
@ -309,12 +266,10 @@ function Connection:create_index(index_name, table_name, columns, unique)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Connection:drop_index(index_name)
|
function Connection:drop_index(index_name)
|
||||||
if string.is_blank(index_name) then
|
if index_name:is_blank() then
|
||||||
error("Index name cannot be empty")
|
error("Index name cannot be empty")
|
||||||
end
|
end
|
||||||
|
return self:exec("DROP INDEX IF EXISTS {{index}}":parse({index = index_name}))
|
||||||
local query = string.template("DROP INDEX IF EXISTS ${index}", {index = index_name})
|
|
||||||
return self:exec(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- SQLite-specific functions
|
-- SQLite-specific functions
|
||||||
@ -332,29 +287,29 @@ end
|
|||||||
|
|
||||||
function Connection:foreign_keys(enabled)
|
function Connection:foreign_keys(enabled)
|
||||||
local value = enabled and "ON" or "OFF"
|
local value = enabled and "ON" or "OFF"
|
||||||
return self:exec(string.template("PRAGMA foreign_keys = ${value}", {value = value}))
|
return self:exec("PRAGMA foreign_keys = {{value}}":parse({value = value}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:journal_mode(mode)
|
function Connection:journal_mode(mode)
|
||||||
mode = mode or "WAL"
|
mode = (mode or "WAL"):upper()
|
||||||
local valid_modes = {"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}
|
local valid_modes = {"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}
|
||||||
|
|
||||||
if not table.contains(table.map(valid_modes, string.upper), string.upper(mode)) then
|
if not table.contains(valid_modes, mode) then
|
||||||
error("Invalid journal mode: " .. mode)
|
error("Invalid journal mode: " .. mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self:query(string.template("PRAGMA journal_mode = ${mode}", {mode = string.upper(mode)}))
|
return self:query("PRAGMA journal_mode = {{mode}}":parse({mode = mode}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:synchronous(level)
|
function Connection:synchronous(level)
|
||||||
level = level or "NORMAL"
|
level = (level or "NORMAL"):upper()
|
||||||
local valid_levels = {"OFF", "NORMAL", "FULL", "EXTRA"}
|
local valid_levels = {"OFF", "NORMAL", "FULL", "EXTRA"}
|
||||||
|
|
||||||
if not table.contains(valid_levels, string.upper(level)) then
|
if not table.contains(valid_levels, level) then
|
||||||
error("Invalid synchronous level: " .. level)
|
error("Invalid synchronous level: " .. level)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self:exec(string.template("PRAGMA synchronous = ${level}", {level = string.upper(level)}))
|
return self:exec("PRAGMA synchronous = {{level}}":parse({level = level}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:cache_size(size)
|
function Connection:cache_size(size)
|
||||||
@ -362,35 +317,30 @@ function Connection:cache_size(size)
|
|||||||
if type(size) ~= "number" then
|
if type(size) ~= "number" then
|
||||||
error("Cache size must be a number")
|
error("Cache size must be a number")
|
||||||
end
|
end
|
||||||
return self:exec(string.template("PRAGMA cache_size = ${size}", {size = tostring(size)}))
|
return self:exec("PRAGMA cache_size = {{size}}":parse({size = tostring(size)}))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Connection:temp_store(mode)
|
function Connection:temp_store(mode)
|
||||||
mode = mode or "MEMORY"
|
mode = (mode or "MEMORY"):upper()
|
||||||
local valid_modes = {"DEFAULT", "FILE", "MEMORY"}
|
local valid_modes = {"DEFAULT", "FILE", "MEMORY"}
|
||||||
|
|
||||||
if not table.contains(valid_modes, string.upper(mode)) then
|
if not table.contains(valid_modes, mode) then
|
||||||
error("Invalid temp_store mode: " .. mode)
|
error("Invalid temp_store mode: " .. mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
return self:exec(string.template("PRAGMA temp_store = ${mode}", {mode = string.upper(mode)}))
|
return self:exec("PRAGMA temp_store = {{mode}}":parse({mode = mode}))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Connection management
|
-- Connection management
|
||||||
function sqlite.open(database_path)
|
function sqlite.open(database_path)
|
||||||
database_path = database_path or ":memory:"
|
database_path = database_path or ":memory:"
|
||||||
|
if database_path ~= ":memory:" and database_path:is_blank() then
|
||||||
if database_path ~= ":memory:" then
|
|
||||||
if string.is_blank(database_path) then
|
|
||||||
database_path = ":memory:"
|
database_path = ":memory:"
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local conn_id = moonshark.sql_connect("sqlite", database_path:trim())
|
local conn_id = moonshark.sql_connect("sqlite", database_path:trim())
|
||||||
if conn_id then
|
if conn_id then
|
||||||
local conn = {_id = conn_id}
|
return setmetatable({_id = conn_id}, Connection)
|
||||||
setmetatable(conn, Connection)
|
|
||||||
return conn
|
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@ -401,9 +351,7 @@ sqlite.connect = sqlite.open
|
|||||||
function sqlite.query(database_path, query_str, ...)
|
function sqlite.query(database_path, query_str, ...)
|
||||||
local conn = sqlite.open(database_path)
|
local conn = sqlite.open(database_path)
|
||||||
if not conn then
|
if not conn then
|
||||||
error(string.template("Failed to open SQLite database: ${path}", {
|
error("Failed to open SQLite database: {{path}}":parse({path = database_path or ":memory:"}))
|
||||||
path = database_path or ":memory:"
|
|
||||||
}))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local results = conn:query(query_str, ...)
|
local results = conn:query(query_str, ...)
|
||||||
@ -414,9 +362,7 @@ end
|
|||||||
function sqlite.exec(database_path, query_str, ...)
|
function sqlite.exec(database_path, query_str, ...)
|
||||||
local conn = sqlite.open(database_path)
|
local conn = sqlite.open(database_path)
|
||||||
if not conn then
|
if not conn then
|
||||||
error(string.template("Failed to open SQLite database: ${path}", {
|
error("Failed to open SQLite database: {{path}}":parse({path = database_path or ":memory:"}))
|
||||||
path = database_path or ":memory:"
|
|
||||||
}))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local result = conn:exec(query_str, ...)
|
local result = conn:exec(query_str, ...)
|
||||||
@ -426,10 +372,7 @@ end
|
|||||||
|
|
||||||
function sqlite.query_row(database_path, query_str, ...)
|
function sqlite.query_row(database_path, query_str, ...)
|
||||||
local results = sqlite.query(database_path, query_str, ...)
|
local results = sqlite.query(database_path, query_str, ...)
|
||||||
if results and #results > 0 then
|
return results and #results > 0 and results[1] or nil
|
||||||
return results[1]
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function sqlite.query_value(database_path, query_str, ...)
|
function sqlite.query_value(database_path, query_str, ...)
|
||||||
@ -449,8 +392,7 @@ function sqlite.migrate(database_path, migrations)
|
|||||||
error("Failed to open SQLite database for migration")
|
error("Failed to open SQLite database for migration")
|
||||||
end
|
end
|
||||||
|
|
||||||
conn:create_table("_migrations",
|
conn:create_table("_migrations", "id INTEGER PRIMARY KEY, name TEXT UNIQUE, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP")
|
||||||
"id INTEGER PRIMARY KEY, name TEXT UNIQUE, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP")
|
|
||||||
|
|
||||||
local tx = conn:begin()
|
local tx = conn:begin()
|
||||||
if not tx then
|
if not tx then
|
||||||
@ -458,18 +400,14 @@ function sqlite.migrate(database_path, migrations)
|
|||||||
error("Failed to begin migration transaction")
|
error("Failed to begin migration transaction")
|
||||||
end
|
end
|
||||||
|
|
||||||
local success = true
|
|
||||||
local error_msg = ""
|
|
||||||
|
|
||||||
for _, migration in ipairs(migrations) do
|
for _, migration in ipairs(migrations) do
|
||||||
if not migration.name or string.is_blank(migration.name) then
|
if not migration.name or migration.name:is_blank() then
|
||||||
error_msg = "Migration must have a non-empty name"
|
tx:rollback()
|
||||||
success = false
|
conn:close()
|
||||||
break
|
error("Migration must have a non-empty name")
|
||||||
end
|
end
|
||||||
|
|
||||||
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?",
|
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?", migration.name:trim())
|
||||||
string.trim(migration.name))
|
|
||||||
if not existing then
|
if not existing then
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
if type(migration.up) == "string" then
|
if type(migration.up) == "string" then
|
||||||
@ -482,52 +420,34 @@ function sqlite.migrate(database_path, migrations)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if ok then
|
if ok then
|
||||||
conn:exec("INSERT INTO _migrations (name) VALUES (?)", string.trim(migration.name))
|
conn:exec("INSERT INTO _migrations (name) VALUES (?)", migration.name:trim())
|
||||||
print(string.template("Applied migration: ${name}", {name = migration.name}))
|
print("Applied migration: {{name}}":parse({name = migration.name}))
|
||||||
else
|
|
||||||
success = false
|
|
||||||
error_msg = string.template("Migration '${name}' failed: ${error}", {
|
|
||||||
name = migration.name,
|
|
||||||
error = err or "unknown error"
|
|
||||||
})
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if success then
|
|
||||||
tx:commit()
|
|
||||||
else
|
else
|
||||||
tx:rollback()
|
tx:rollback()
|
||||||
conn:close()
|
conn:close()
|
||||||
error(error_msg)
|
error("Migration '{{name}}' failed: {{error}}":parse({
|
||||||
|
name = migration.name,
|
||||||
|
error = err or "unknown error"
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tx:commit()
|
||||||
conn:close()
|
conn:close()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified result processing using table utilities
|
-- Result processing utilities
|
||||||
function sqlite.to_array(results, column_name)
|
function sqlite.to_array(results, column_name)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then return {} end
|
||||||
return {}
|
if column_name:is_blank() then error("Column name cannot be empty") end
|
||||||
end
|
|
||||||
|
|
||||||
if string.is_blank(column_name) then
|
|
||||||
error("Column name cannot be empty")
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.map(results, function(row) return row[column_name] end)
|
return table.map(results, function(row) return row[column_name] end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function sqlite.to_map(results, key_column, value_column)
|
function sqlite.to_map(results, key_column, value_column)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then return {} end
|
||||||
return {}
|
if key_column:is_blank() then error("Key column name cannot be empty") end
|
||||||
end
|
|
||||||
|
|
||||||
if string.is_blank(key_column) then
|
|
||||||
error("Key column name cannot be empty")
|
|
||||||
end
|
|
||||||
|
|
||||||
local map = {}
|
local map = {}
|
||||||
for _, row in ipairs(results) do
|
for _, row in ipairs(results) do
|
||||||
@ -538,18 +458,11 @@ function sqlite.to_map(results, key_column, value_column)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function sqlite.group_by(results, column_name)
|
function sqlite.group_by(results, column_name)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then return {} end
|
||||||
return {}
|
if column_name:is_blank() then error("Column name cannot be empty") end
|
||||||
end
|
|
||||||
|
|
||||||
if string.is_blank(column_name) then
|
|
||||||
error("Column name cannot be empty")
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.group_by(results, function(row) return row[column_name] end)
|
return table.group_by(results, function(row) return row[column_name] end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Simplified debug helper
|
|
||||||
function sqlite.print_results(results)
|
function sqlite.print_results(results)
|
||||||
if not results or table.is_empty(results) then
|
if not results or table.is_empty(results) then
|
||||||
print("No results")
|
print("No results")
|
||||||
@ -560,17 +473,17 @@ function sqlite.print_results(results)
|
|||||||
table.sort(columns)
|
table.sort(columns)
|
||||||
|
|
||||||
-- Calculate column widths
|
-- Calculate column widths
|
||||||
local widths = table.map_values(table.to_map(columns, function(col) return col end, function(col) return string.length(col) end), function(width) return width end)
|
local widths = {}
|
||||||
|
|
||||||
for _, row in ipairs(results) do
|
|
||||||
for _, col in ipairs(columns) do
|
for _, col in ipairs(columns) do
|
||||||
|
widths[col] = col:length()
|
||||||
|
for _, row in ipairs(results) do
|
||||||
local value = tostring(row[col] or "")
|
local value = tostring(row[col] or "")
|
||||||
widths[col] = math.max(widths[col], string.length(value))
|
widths[col] = math.max(widths[col], value:length())
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Print header
|
-- Print header and separator
|
||||||
local header_parts = table.map(columns, function(col) return string.pad_right(col, widths[col]) end)
|
local header_parts = table.map(columns, function(col) return col:pad_right(widths[col]) end)
|
||||||
local separator_parts = table.map(columns, function(col) return string.repeat_("-", widths[col]) end)
|
local separator_parts = table.map(columns, function(col) return string.repeat_("-", widths[col]) end)
|
||||||
|
|
||||||
print(table.concat(header_parts, " | "))
|
print(table.concat(header_parts, " | "))
|
||||||
@ -580,7 +493,7 @@ function sqlite.print_results(results)
|
|||||||
for _, row in ipairs(results) do
|
for _, row in ipairs(results) do
|
||||||
local value_parts = table.map(columns, function(col)
|
local value_parts = table.map(columns, function(col)
|
||||||
local value = tostring(row[col] or "")
|
local value = tostring(row[col] or "")
|
||||||
return string.pad_right(value, widths[col])
|
return value:pad_right(widths[col])
|
||||||
end)
|
end)
|
||||||
print(table.concat(value_parts, " | "))
|
print(table.concat(value_parts, " | "))
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user