From 041b4a517dcfeaac2cb2cfc4859509f0d4ec6d18 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 1 Aug 2025 15:21:43 -0500 Subject: [PATCH] improve string library usage in the database libraries --- modules/mysql/mysql.lua | 519 +++++++++++++--------------------- modules/postgres/postgres.lua | 428 ++++++++++------------------ modules/sqlite/sqlite.lua | 283 +++++++----------- 3 files changed, 445 insertions(+), 785 deletions(-) diff --git a/modules/mysql/mysql.lua b/modules/mysql/mysql.lua index 3b1dccd..bd56820 100644 --- a/modules/mysql/mysql.lua +++ b/modules/mysql/mysql.lua @@ -23,24 +23,19 @@ function Connection:query(query_str, ...) if not self._id then error("Connection is closed") end - query_str = string.normalize_whitespace(query_str) - return moonshark.sql_query(self._id, query_str, ...) + return moonshark.sql_query(self._id, query_str:normalize_whitespace(), ...) end function Connection:exec(query_str, ...) if not self._id then error("Connection is closed") end - query_str = string.normalize_whitespace(query_str) - return moonshark.sql_exec(self._id, query_str, ...) + return moonshark.sql_exec(self._id, query_str:normalize_whitespace(), ...) end function Connection:query_row(query_str, ...) local results = self:query(query_str, ...) - if results and #results > 0 then - return results[1] - end - return nil + return results and #results > 0 and results[1] or nil end function Connection:query_value(query_str, ...) @@ -53,77 +48,50 @@ function Connection:query_value(query_str, ...) return nil end --- Enhanced transaction support with savepoints function Connection:begin() local result = self:exec("BEGIN") if result then return { conn = self, active = true, - commit = function(tx) if tx.active then - local result = tx.conn:exec("COMMIT") tx.active = false - return result + return tx.conn:exec("COMMIT") end return false end, - rollback = function(tx) if tx.active then - local result = tx.conn:exec("ROLLBACK") tx.active = false - return result + return tx.conn:exec("ROLLBACK") end return false end, - savepoint = function(tx, name) - if not tx.active then - error("Transaction is not active") - end - if string.is_blank(name) then - error("Savepoint name cannot be empty") - end - return tx.conn:exec(string.template("SAVEPOINT ${name}", {name = name})) + if not tx.active then error("Transaction is not active") end + if name:is_blank() then error("Savepoint name cannot be empty") end + return tx.conn:exec("SAVEPOINT {{name}}":parse({name = name})) end, - rollback_to = function(tx, name) - if not tx.active then - error("Transaction is not active") - end - 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})) + if not tx.active then error("Transaction is not active") end + if name:is_blank() then error("Savepoint name cannot be empty") end + return tx.conn:exec("ROLLBACK TO SAVEPOINT {{name}}":parse({name = name})) end, - query = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query(query_str, ...) end, - exec = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:exec(query_str, ...) end, - query_row = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query_row(query_str, ...) end, - query_value = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query_value(query_str, ...) end } @@ -131,104 +99,98 @@ function Connection:begin() return nil end --- Simplified MySQL-specific query builder helpers 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") end local keys = table.keys(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, - columns = table.concat(keys, ", "), - placeholders = table.concat(placeholders, ", ") + columns = keys:join(", "), + placeholders = placeholders }) return self:exec(query, unpack(values)) end function Connection:upsert(table_name, data, update_data) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty") end local keys = table.keys(data) local values = table.values(data) - local placeholders = table.map(keys, function() return "?" end) + local placeholders = string.repeat_("?, ", #keys):trim_right(", ") -- Use update_data if provided, otherwise update with same data local update_source = update_data or data local updates = table.map(table.keys(update_source), function(key) - return string.template("${key} = VALUES(${key})", {key = key}) + return key .. " = VALUES(" .. key .. ")" end) - local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}", { + local query = "INSERT INTO {{table}} ({{columns}}) VALUES ({{placeholders}}) ON DUPLICATE KEY UPDATE {{updates}}":parse({ table = table_name, - columns = table.concat(keys, ", "), - placeholders = table.concat(placeholders, ", "), - updates = table.concat(updates, ", ") + columns = keys:join(", "), + placeholders = placeholders, + updates = updates:join(", ") }) return self:exec(query, unpack(values)) end function Connection:replace(table_name, data) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty") end local keys = table.keys(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("REPLACE INTO ${table} (${columns}) VALUES (${placeholders})", { + local query = "REPLACE INTO {{table}} ({{columns}}) VALUES ({{placeholders}})":parse({ table = table_name, - columns = table.concat(keys, ", "), - placeholders = table.concat(placeholders, ", ") + columns = keys:join(", "), + placeholders = placeholders }) return self:exec(query, unpack(values)) end 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") end - if string.is_blank(where_clause) then + if where_clause:is_blank() then error("WHERE clause cannot be empty for UPDATE") end local keys = table.keys(data) local values = table.values(data) - local sets = table.map(keys, function(key) - return string.template("${key} = ?", {key = key}) - end) + local sets = table.map(keys, function(key) return key .. " = ?" end) - local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", { + local query = "UPDATE {{table}} SET {{sets}} WHERE {{where}}":parse({ table = table_name, - sets = table.concat(sets, ", "), + sets = sets:join(", "), where = where_clause }) - -- Add WHERE clause parameters - local where_args = {...} - table.extend(values, where_args) - + table.extend(values, {...}) return self:exec(query, unpack(values)) end 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") end - if string.is_blank(where_clause) then + if where_clause:is_blank() then error("WHERE clause cannot be empty for DELETE") end - local query = string.template("DELETE FROM ${table} WHERE ${where}", { + local query = "DELETE FROM {{table}} WHERE {{where}}":parse({ table = table_name, where = where_clause }) @@ -236,7 +198,7 @@ function Connection:delete(table_name, where_clause, ...) end 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") end @@ -245,16 +207,15 @@ function Connection:select(table_name, columns, where_clause, ...) columns = table.concat(columns, ", ") end - local query - if where_clause and not string.is_blank(where_clause) then - query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", { + if where_clause and not where_clause:is_blank() then + local query = "SELECT {{columns}} FROM {{table}} WHERE {{where}}":parse({ columns = columns, table = table_name, where = where_clause }) return self:query(query, ...) else - query = string.template("SELECT ${columns} FROM ${table}", { + local query = "SELECT {{columns}} FROM {{table}}":parse({ columns = columns, table = table_name }) @@ -264,230 +225,191 @@ end -- MySQL schema helpers function Connection:database_exists(database_name) - if string.is_blank(database_name) then - return false - end - - local result = self:query_value( - "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", - string.trim(database_name) - ) - return result ~= nil + if database_name:is_blank() then return false end + return self:query_value("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", + database_name:trim()) ~= nil end function Connection:table_exists(table_name, database_name) - if string.is_blank(table_name) then - return false - end - + if table_name:is_blank() then return false end database_name = database_name or self:current_database() - if not database_name then - return false - end + if not database_name then return false end - local result = self:query_value( - "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", - string.trim(database_name), string.trim(table_name) - ) - return result ~= nil + return self:query_value("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", + database_name:trim(), table_name:trim()) ~= nil end function Connection:column_exists(table_name, column_name, database_name) - if string.is_blank(table_name) or string.is_blank(column_name) then - return false - end - + if table_name:is_blank() or column_name:is_blank() then return false end database_name = database_name or self:current_database() - if not database_name then - return false - end + if not database_name then return false end - local result = self:query_value([[ + return self:query_value([[ SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? - ]], string.trim(database_name), string.trim(table_name), string.trim(column_name)) - return result ~= nil + ]], database_name:trim(), table_name:trim(), column_name:trim()) ~= nil end function Connection:create_database(database_name, charset, collation) - if string.is_blank(database_name) then + if database_name:is_blank() then error("Database name cannot be empty") end - local charset_clause = charset and string.template(" CHARACTER SET ${charset}", {charset = charset}) or "" - local collation_clause = collation and string.template(" COLLATE ${collation}", {collation = collation}) or "" + local charset_clause = charset and " CHARACTER SET " .. charset or "" + local collation_clause = collation and " COLLATE " .. collation or "" - local query = string.template("CREATE DATABASE IF NOT EXISTS ${database}${charset}${collation}", { + return self:exec("CREATE DATABASE IF NOT EXISTS {{database}}{{charset}}{{collation}}":parse({ database = database_name, charset = charset_clause, collation = collation_clause - }) - return self:exec(query) + })) end function Connection:drop_database(database_name) - if string.is_blank(database_name) then + if database_name:is_blank() then error("Database name cannot be empty") end - - local query = string.template("DROP DATABASE IF EXISTS ${database}", {database = database_name}) - return self:exec(query) + return self:exec("DROP DATABASE IF EXISTS {{database}}":parse({database = database_name})) end function Connection:create_table(table_name, schema, engine, charset) - 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") end - local engine_clause = engine and string.template(" ENGINE=${engine}", {engine = string.upper(engine)}) or "" - local charset_clause = charset and string.template(" CHARACTER SET ${charset}", {charset = charset}) or "" + local engine_clause = engine and " ENGINE=" .. engine:upper() or "" + local charset_clause = charset and " CHARACTER SET " .. charset or "" - local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})${engine}${charset}", { + return self:exec("CREATE TABLE IF NOT EXISTS {{table}} ({{schema}}){{engine}}{{charset}}":parse({ table = table_name, - schema = string.trim(schema), + schema = schema:trim(), engine = engine_clause, charset = charset_clause - }) - return self:exec(query) + })) end 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") end - - local query = string.template("DROP TABLE IF EXISTS ${table}", {table = table_name}) - return self:exec(query) + return self:exec("DROP TABLE IF EXISTS {{table}}":parse({table = table_name})) end function Connection:add_column(table_name, column_def, position) - 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") end - local position_clause = position and string.template(" ${position}", {position = position}) or "" - local query = string.template("ALTER TABLE ${table} ADD COLUMN ${column}${position}", { + local position_clause = position and " " .. position or "" + return self:exec("ALTER TABLE {{table}} ADD COLUMN {{column}}{{position}}":parse({ table = table_name, - column = string.trim(column_def), + column = column_def:trim(), position = position_clause - }) - return self:exec(query) + })) end function Connection:drop_column(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 error("Table name and column name cannot be empty") end - - local query = string.template("ALTER TABLE ${table} DROP COLUMN ${column}", { + return self:exec("ALTER TABLE {{table}} DROP COLUMN {{column}}":parse({ table = table_name, column = column_name - }) - return self:exec(query) + })) end function Connection:modify_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") end - - local query = string.template("ALTER TABLE ${table} MODIFY COLUMN ${column}", { + return self:exec("ALTER TABLE {{table}} MODIFY COLUMN {{column}}":parse({ table = table_name, - column = string.trim(column_def) - }) - return self:exec(query) + column = column_def:trim() + })) end function Connection:rename_table(old_name, new_name) - if string.is_blank(old_name) or string.is_blank(new_name) then + if old_name:is_blank() or new_name:is_blank() then error("Old and new table names cannot be empty") end - - local query = string.template("RENAME TABLE ${old} TO ${new}", { - old = old_name, - new = new_name - }) - return self:exec(query) + return self:exec("RENAME TABLE {{old}} TO {{new}}":parse({old = old_name, new = new_name})) end function Connection:create_index(index_name, table_name, columns, unique, type) - 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") end local unique_clause = unique and "UNIQUE " or "" - local type_clause = type and string.template(" USING ${type}", {type = string.upper(type)}) or "" + local type_clause = type and " USING " .. type:upper() or "" local columns_str = type(columns) == "table" and table.concat(columns, ", ") or tostring(columns) - local query = string.template("CREATE ${unique}INDEX ${index} ON ${table} (${columns})${type}", { + return self:exec("CREATE {{unique}}INDEX {{index}} ON {{table}} ({{columns}}){{type}}":parse({ unique = unique_clause, index = index_name, table = table_name, columns = columns_str, type = type_clause - }) - return self:exec(query) + })) end function Connection:drop_index(index_name, table_name) - 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") end - - local query = string.template("DROP INDEX ${index} ON ${table}", { + return self:exec("DROP INDEX {{index}} ON {{table}}":parse({ index = index_name, table = table_name - }) - return self:exec(query) + })) end -- MySQL maintenance functions function Connection:optimize(table_name) - local table_clause = table_name and string.template(" ${table}", {table = table_name}) or "" - return self:query(string.template("OPTIMIZE TABLE${table}", {table = table_clause})) + local table_clause = table_name and " " .. table_name or "" + return self:query("OPTIMIZE TABLE{{table}}":parse({table = table_clause})) end function Connection:repair(table_name) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty for REPAIR") end - return self:query(string.template("REPAIR TABLE ${table}", {table = table_name})) + return self:query("REPAIR TABLE {{table}}":parse({table = table_name})) end function Connection:check_table(table_name, options) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty for CHECK") end local options_clause = "" if options then local valid_options = {"QUICK", "FAST", "MEDIUM", "EXTENDED", "CHANGED"} - local options_upper = string.upper(options) + local options_upper = options:upper() if table.contains(valid_options, options_upper) then - options_clause = string.template(" ${options}", {options = options_upper}) + options_clause = " " .. options_upper end end - return self:query(string.template("CHECK TABLE ${table}${options}", { + return self:query("CHECK TABLE {{table}}{{options}}":parse({ table = table_name, options = options_clause })) end function Connection:analyze_table(table_name) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty for ANALYZE") end - return self:query(string.template("ANALYZE TABLE ${table}", {table = table_name})) + return self:query("ANALYZE TABLE {{table}}":parse({table = table_name})) end -- MySQL settings and introspection function Connection:show(what) - if string.is_blank(what) then + if what:is_blank() then error("SHOW parameter cannot be empty") end - return self:query(string.template("SHOW ${what}", {what = string.upper(what)})) + return self:query("SHOW {{what}}":parse({what = what:upper()})) end function Connection:current_database() @@ -507,36 +429,36 @@ function Connection:list_databases() end function Connection:list_tables(database_name) - if database_name and not string.is_blank(database_name) then - return self:query(string.template("SHOW TABLES FROM ${database}", {database = database_name})) + if database_name and not database_name:is_blank() then + return self:query("SHOW TABLES FROM {{database}}":parse({database = database_name})) else return self:query("SHOW TABLES") end end function Connection:describe_table(table_name) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty") end - return self:query(string.template("DESCRIBE ${table}", {table = table_name})) + return self:query("DESCRIBE {{table}}":parse({table = table_name})) end function Connection:show_create_table(table_name) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty") end - return self:query(string.template("SHOW CREATE TABLE ${table}", {table = table_name})) + return self:query("SHOW CREATE TABLE {{table}}":parse({table = table_name})) end function Connection:show_indexes(table_name) - if string.is_blank(table_name) then + if table_name:is_blank() then error("Table name cannot be empty") end - return self:query(string.template("SHOW INDEXES FROM ${table}", {table = table_name})) + return self:query("SHOW INDEXES FROM {{table}}":parse({table = table_name})) end function Connection:show_table_status(table_name) - if table_name and not string.is_blank(table_name) then + if table_name and not table_name:is_blank() then return self:query("SHOW TABLE STATUS LIKE ?", table_name) else return self:query("SHOW TABLE STATUS") @@ -545,65 +467,61 @@ end -- MySQL user and privilege management function Connection:create_user(username, password, host) - if string.is_blank(username) or string.is_blank(password) then + if username:is_blank() or password:is_blank() then error("Username and password cannot be empty") end host = host or "%" - local query = string.template("CREATE USER '${username}'@'${host}' IDENTIFIED BY ?", { + return self:exec("CREATE USER '{{username}}'@'{{host}}' IDENTIFIED BY ?":parse({ username = username, host = host - }) - return self:exec(query, password) + }), password) end function Connection:drop_user(username, host) - if string.is_blank(username) then + if username:is_blank() then error("Username cannot be empty") end host = host or "%" - local query = string.template("DROP USER IF EXISTS '${username}'@'${host}'", { + return self:exec("DROP USER IF EXISTS '{{username}}'@'{{host}}'":parse({ username = username, host = host - }) - return self:exec(query) + })) end function Connection:grant(privileges, database, table_name, username, host) - if string.is_blank(privileges) or string.is_blank(database) or string.is_blank(username) then + if privileges:is_blank() or database:is_blank() or username:is_blank() then error("Privileges, database, and username cannot be empty") end host = host or "%" table_name = table_name or "*" - local object = string.template("${database}.${table}", {database = database, table = table_name}) + local object = database .. "." .. table_name - local query = string.template("GRANT ${privileges} ON ${object} TO '${username}'@'${host}'", { - privileges = string.upper(privileges), + return self:exec("GRANT {{privileges}} ON {{object}} TO '{{username}}'@'{{host}}'":parse({ + privileges = privileges:upper(), object = object, username = username, host = host - }) - return self:exec(query) + })) end function Connection:revoke(privileges, database, table_name, username, host) - if string.is_blank(privileges) or string.is_blank(database) or string.is_blank(username) then + if privileges:is_blank() or database:is_blank() or username:is_blank() then error("Privileges, database, and username cannot be empty") end host = host or "%" table_name = table_name or "*" - local object = string.template("${database}.${table}", {database = database, table = table_name}) + local object = database .. "." .. table_name - local query = string.template("REVOKE ${privileges} ON ${object} FROM '${username}'@'${host}'", { - privileges = string.upper(privileges), + return self:exec("REVOKE {{privileges}} ON {{object}} FROM '{{username}}'@'{{host}}'":parse({ + privileges = privileges:upper(), object = object, username = username, host = host - }) - return self:exec(query) + })) end function Connection:flush_privileges() @@ -612,31 +530,25 @@ end -- MySQL variables and configuration function Connection:set_variable(name, value, global) - if string.is_blank(name) then + if name:is_blank() then error("Variable name cannot be empty") end local scope = global and "GLOBAL " or "SESSION " - return self:exec(string.template("SET ${scope}${name} = ?", { - scope = scope, - name = name - }), value) + return self:exec("SET {{scope}}{{name}} = ?":parse({scope = scope, name = name}), value) end function Connection:get_variable(name, global) - if string.is_blank(name) then + if name:is_blank() then error("Variable name cannot be empty") end local scope = global and "global." or "session." - return self:query_value(string.template("SELECT @@${scope}${name}", { - scope = scope, - name = name - })) + return self:query_value("SELECT @@{{scope}}{{name}}":parse({scope = scope, name = name})) end function Connection:show_variables(pattern) - if pattern and not string.is_blank(pattern) then + if pattern and not pattern:is_blank() then return self:query("SHOW VARIABLES LIKE ?", pattern) else return self:query("SHOW VARIABLES") @@ -644,7 +556,7 @@ function Connection:show_variables(pattern) end function Connection:show_status(pattern) - if pattern and not string.is_blank(pattern) then + if pattern and not pattern:is_blank() then return self:query("SHOW STATUS LIKE ?", pattern) else return self:query("SHOW STATUS") @@ -653,15 +565,13 @@ end -- Connection management function mysql.connect(dsn) - if string.is_blank(dsn) then + if dsn:is_blank() then error("DSN cannot be empty") end - local conn_id = moonshark.sql_connect("mysql", string.trim(dsn)) + local conn_id = moonshark.sql_connect("mysql", dsn:trim()) if conn_id then - local conn = {_id = conn_id} - setmetatable(conn, Connection) - return conn + return setmetatable({_id = conn_id}, Connection) end return nil end @@ -693,10 +603,7 @@ end function mysql.query_row(dsn, query_str, ...) local results = mysql.query(dsn, query_str, ...) - if results and #results > 0 then - return results[1] - end - return nil + return results and #results > 0 and results[1] or nil end function mysql.query_value(dsn, query_str, ...) @@ -717,13 +624,12 @@ function mysql.migrate(dsn, migrations, database_name) end -- Use specified database if provided - if database_name and not string.is_blank(database_name) then - conn:exec(string.template("USE ${database}", {database = database_name})) + if database_name and not database_name:is_blank() then + conn:exec("USE {{database}}":parse({database = database_name})) end -- Create migrations table - conn:create_table("_migrations", - "id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) UNIQUE NOT NULL, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + conn:create_table("_migrations", "id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) UNIQUE NOT NULL, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP") local tx = conn:begin() if not tx then @@ -731,19 +637,15 @@ function mysql.migrate(dsn, migrations, database_name) error("Failed to begin migration transaction") end - local success = true - local error_msg = "" - for _, migration in ipairs(migrations) do - if not migration.name or string.is_blank(migration.name) then - error_msg = "Migration must have a non-empty name" - success = false - break + if not migration.name or migration.name:is_blank() then + tx:rollback() + conn:close() + error("Migration must have a non-empty name") end -- Check if migration already applied - local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?", - string.trim(migration.name)) + local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?", migration.name:trim()) if not existing then local ok, err = pcall(function() if type(migration.up) == "string" then @@ -756,52 +658,34 @@ function mysql.migrate(dsn, migrations, database_name) end) if ok then - conn:exec("INSERT INTO _migrations (name) VALUES (?)", string.trim(migration.name)) - print(string.template("Applied migration: ${name}", {name = migration.name})) + conn:exec("INSERT INTO _migrations (name) VALUES (?)", migration.name:trim()) + print("Applied migration: {{name}}":parse({name = migration.name})) else - success = false - error_msg = string.template("Migration '${name}' failed: ${error}", { + tx:rollback() + conn:close() + error("Migration '{{name}}' failed: {{error}}":parse({ name = migration.name, error = err or "unknown error" - }) - break + })) end end end - if success then - tx:commit() - else - tx:rollback() - conn:close() - error(error_msg) - end - + tx:commit() conn:close() return true end --- Simplified result processing utilities +-- Result processing utilities function mysql.to_array(results, column_name) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(column_name) then - error("Column name cannot be empty") - end - + if not results or table.is_empty(results) then return {} end + if column_name:is_blank() then error("Column name cannot be empty") end return table.map(results, function(row) return row[column_name] end) end function mysql.to_map(results, key_column, value_column) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(key_column) then - error("Key column name cannot be empty") - end + if not results or table.is_empty(results) then return {} end + if key_column:is_blank() then error("Key column name cannot be empty") end local map = {} for _, row in ipairs(results) do @@ -812,18 +696,11 @@ function mysql.to_map(results, key_column, value_column) end function mysql.group_by(results, column_name) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(column_name) then - error("Column name cannot be empty") - end - + if not results or table.is_empty(results) then return {} end + if column_name:is_blank() then error("Column name cannot be empty") end return table.group_by(results, function(row) return row[column_name] end) end --- Simplified debug helper function mysql.print_results(results) if not results or table.is_empty(results) then print("No results") @@ -836,18 +713,15 @@ function mysql.print_results(results) -- Calculate column widths local widths = {} for _, col in ipairs(columns) do - widths[col] = string.length(col) - end - - for _, row in ipairs(results) do - for _, col in ipairs(columns) do + widths[col] = col:length() + for _, row in ipairs(results) do 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 - -- Print header - local header_parts = table.map(columns, function(col) return string.pad_right(col, widths[col]) end) + -- Print header and separator + 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) print(table.concat(header_parts, " | ")) @@ -857,7 +731,7 @@ function mysql.print_results(results) for _, row in ipairs(results) do local value_parts = table.map(columns, function(col) local value = tostring(row[col] or "") - return string.pad_right(value, widths[col]) + return value:pad_right(widths[col]) end) print(table.concat(value_parts, " | ")) end @@ -868,14 +742,14 @@ function mysql.escape_string(str_val) if type(str_val) ~= "string" then return tostring(str_val) end - return string.replace(str_val, "'", "\\'") + return str_val:replace("'", "\\'") end function mysql.escape_identifier(name) - if string.is_blank(name) then + if name:is_blank() then error("Identifier name cannot be empty") end - return string.template("`${name}`", {name = string.replace(name, "`", "``")}) + return "`{{name}}`":parse({name = name:replace("`", "``")}) end -- DSN builder helper @@ -886,61 +760,52 @@ function mysql.build_dsn(options) local parts = {} - if options.username and not string.is_blank(options.username) then + if options.username and not options.username:is_blank() then table.insert(parts, options.username) - if options.password and not string.is_blank(options.password) then - parts[#parts] = string.template("${user}:${pass}", { - user = parts[#parts], - pass = options.password - }) + if options.password and not options.password:is_blank() then + parts[#parts] = parts[#parts] .. ":" .. options.password end parts[#parts] = parts[#parts] .. "@" end - if options.protocol and not string.is_blank(options.protocol) then - table.insert(parts, string.template("${protocol}(", {protocol = options.protocol})) - if options.host and not string.is_blank(options.host) then - table.insert(parts, options.host) + if options.protocol and not options.protocol:is_blank() then + local host_part = options.protocol .. "(" + if options.host and not options.host:is_blank() then + host_part = host_part .. options.host if options.port then - parts[#parts] = string.template("${host}:${port}", { - host = parts[#parts], - port = tostring(options.port) - }) + host_part = host_part .. ":" .. tostring(options.port) end end - parts[#parts] = parts[#parts] .. ")" - elseif options.host and not string.is_blank(options.host) then - local host_part = string.template("tcp(${host}", {host = options.host}) + table.insert(parts, host_part .. ")") + elseif options.host and not options.host:is_blank() then + local host_part = "tcp(" .. options.host if options.port then - host_part = string.template("${host}:${port}", { - host = host_part, - port = tostring(options.port) - }) + host_part = host_part .. ":" .. tostring(options.port) end table.insert(parts, host_part .. ")") end - if options.database and not string.is_blank(options.database) then - table.insert(parts, string.template("/${database}", {database = options.database})) + if options.database and not options.database:is_blank() then + table.insert(parts, "/" .. options.database) end -- Add parameters local params = {} - if options.charset and not string.is_blank(options.charset) then - table.insert(params, string.template("charset=${charset}", {charset = options.charset})) + if options.charset and not options.charset:is_blank() then + table.insert(params, "charset=" .. options.charset) end if options.parseTime ~= nil then - table.insert(params, string.template("parseTime=${parse}", {parse = tostring(options.parseTime)})) + table.insert(params, "parseTime=" .. tostring(options.parseTime)) end - if options.timeout and not string.is_blank(options.timeout) then - table.insert(params, string.template("timeout=${timeout}", {timeout = options.timeout})) + if options.timeout and not options.timeout:is_blank() then + table.insert(params, "timeout=" .. options.timeout) end - if options.tls and not string.is_blank(options.tls) then - table.insert(params, string.template("tls=${tls}", {tls = options.tls})) + if options.tls and not options.tls:is_blank() then + table.insert(params, "tls=" .. options.tls) end if #params > 0 then - table.insert(parts, string.template("?${params}", {params = table.concat(params, "&")})) + table.insert(parts, "?" .. table.concat(params, "&")) end return table.concat(parts, "") diff --git a/modules/postgres/postgres.lua b/modules/postgres/postgres.lua index 7e7c681..c931f2f 100644 --- a/modules/postgres/postgres.lua +++ b/modules/postgres/postgres.lua @@ -23,24 +23,19 @@ function Connection:query(query_str, ...) if not self._id then error("Connection is closed") end - query_str = string.normalize_whitespace(query_str) - return moonshark.sql_query(self._id, query_str, ...) + return moonshark.sql_query(self._id, query_str:normalize_whitespace(), ...) end function Connection:exec(query_str, ...) if not self._id then error("Connection is closed") end - query_str = string.normalize_whitespace(query_str) - return moonshark.sql_exec(self._id, query_str, ...) + return moonshark.sql_exec(self._id, query_str:normalize_whitespace(), ...) end function Connection:query_row(query_str, ...) local results = self:query(query_str, ...) - if results and #results > 0 then - return results[1] - end - return nil + return results and #results > 0 and results[1] or nil end function Connection:query_value(query_str, ...) @@ -53,77 +48,50 @@ function Connection:query_value(query_str, ...) return nil end --- Enhanced transaction support with savepoints function Connection:begin() local result = self:exec("BEGIN") if result then return { conn = self, active = true, - commit = function(tx) if tx.active then - local result = tx.conn:exec("COMMIT") tx.active = false - return result + return tx.conn:exec("COMMIT") end return false end, - rollback = function(tx) if tx.active then - local result = tx.conn:exec("ROLLBACK") tx.active = false - return result + return tx.conn:exec("ROLLBACK") end return false end, - savepoint = function(tx, name) - if not tx.active then - error("Transaction is not active") - end - if string.is_blank(name) then - error("Savepoint name cannot be empty") - end - return tx.conn:exec(string.template("SAVEPOINT ${name}", {name = name})) + if not tx.active then error("Transaction is not active") end + if name:is_blank() then error("Savepoint name cannot be empty") end + return tx.conn:exec("SAVEPOINT {{name}}":parse({name = name})) end, - rollback_to = function(tx, name) - if not tx.active then - error("Transaction is not active") - end - 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})) + if not tx.active then error("Transaction is not active") end + if name:is_blank() then error("Savepoint name cannot be empty") end + return tx.conn:exec("ROLLBACK TO SAVEPOINT {{name}}":parse({name = name})) end, - query = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query(query_str, ...) end, - exec = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:exec(query_str, ...) end, - query_row = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query_row(query_str, ...) end, - query_value = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query_value(query_str, ...) end } @@ -131,38 +99,34 @@ function Connection:begin() return nil end --- Simplified PostgreSQL parameter builder +-- Build PostgreSQL parameters ($1, $2, etc.) local function build_postgres_params(data) local keys = table.keys(data) local values = table.values(data) local placeholders = {} for i = 1, #keys do - table.insert(placeholders, string.template("$${num}", {num = tostring(i)})) + placeholders[i] = "$" .. i end - return keys, values, placeholders, #keys + return keys, values, placeholders end --- Simplified query builders using table utilities 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") end 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, - columns = table.concat(keys, ", "), + columns = keys:join(", "), placeholders = table.concat(placeholders, ", ") }) - if returning and not string.is_blank(returning) then - query = string.template("${query} RETURNING ${returning}", { - query = query, - returning = returning - }) + if returning and not returning:is_blank() then + query = query .. " RETURNING " .. returning return self:query(query, unpack(values)) else return self:exec(query, unpack(values)) @@ -170,37 +134,32 @@ function Connection:insert(table_name, data, returning) end 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") end local keys, values, placeholders = build_postgres_params(data) - local updates = table.map(keys, function(key) - return string.template("${key} = EXCLUDED.${key}", {key = key}) - end) + local updates = table.map(keys, function(key) return key .. " = EXCLUDED." .. key end) local conflict_clause = "" if conflict_columns then if type(conflict_columns) == "string" then - conflict_clause = string.template("(${columns})", {columns = conflict_columns}) + conflict_clause = "(" .. conflict_columns .. ")" else - conflict_clause = string.template("(${columns})", {columns = table.concat(conflict_columns, ", ")}) + conflict_clause = "(" .. table.concat(conflict_columns, ", ") .. ")" 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, - columns = table.concat(keys, ", "), + columns = keys:join(", "), placeholders = table.concat(placeholders, ", "), conflict = conflict_clause, - updates = table.concat(updates, ", ") + updates = updates:join(", ") }) - if returning and not string.is_blank(returning) then - query = string.template("${query} RETURNING ${returning}", { - query = query, - returning = returning - }) + if returning and not returning:is_blank() then + query = query .. " RETURNING " .. returning return self:query(query, unpack(values)) else return self:exec(query, unpack(values)) @@ -208,10 +167,10 @@ function Connection:upsert(table_name, data, conflict_columns, returning) end 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") end - if string.is_blank(where_clause) then + if where_clause:is_blank() then error("WHERE clause cannot be empty for UPDATE") end @@ -219,35 +178,29 @@ function Connection:update(table_name, data, where_clause, returning, ...) local values = table.values(data) local param_count = #keys + -- Build SET clause with numbered parameters local sets = {} for i, key in ipairs(keys) do - table.insert(sets, string.template("${key} = $${num}", { - key = key, - num = tostring(i) - })) + sets[i] = key .. " = $" .. i end - -- Handle WHERE clause parameters + -- Handle WHERE parameters local where_args = {...} - local where_clause_with_params = where_clause + local where_clause_final = where_clause for i = 1, #where_args do param_count = param_count + 1 - table.insert(values, where_args[i]) - where_clause_with_params = string.replace(where_clause_with_params, "?", - string.template("$${num}", {num = tostring(param_count)}), 1) + values[#values + 1] = where_args[i] + where_clause_final = where_clause_final:replace("?", "$" .. param_count, 1) end - local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", { + local query = "UPDATE {{table}} SET {{sets}} WHERE {{where}}":parse({ table = table_name, sets = table.concat(sets, ", "), - where = where_clause_with_params + where = where_clause_final }) - if returning and not string.is_blank(returning) then - query = string.template("${query} RETURNING ${returning}", { - query = query, - returning = returning - }) + if returning and not returning:is_blank() then + query = query .. " RETURNING " .. returning return self:query(query, unpack(values)) else return self:exec(query, unpack(values)) @@ -255,33 +208,29 @@ function Connection:update(table_name, data, where_clause, returning, ...) end 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") end - if string.is_blank(where_clause) then + if where_clause:is_blank() then error("WHERE clause cannot be empty for DELETE") end - -- Handle WHERE clause parameters local where_args = {...} local values = {} - local where_clause_with_params = where_clause + local where_clause_final = where_clause + for i = 1, #where_args do - table.insert(values, where_args[i]) - where_clause_with_params = string.replace(where_clause_with_params, "?", - string.template("$${num}", {num = tostring(i)}), 1) + values[i] = where_args[i] + where_clause_final = where_clause_final:replace("?", "$" .. i, 1) end - local query = string.template("DELETE FROM ${table} WHERE ${where}", { + local query = "DELETE FROM {{table}} WHERE {{where}}":parse({ table = table_name, - where = where_clause_with_params + where = where_clause_final }) - if returning and not string.is_blank(returning) then - query = string.template("${query} RETURNING ${returning}", { - query = query, - returning = returning - }) + if returning and not returning:is_blank() then + query = query .. " RETURNING " .. returning return self:query(query, unpack(values)) else return self:exec(query, unpack(values)) @@ -289,7 +238,7 @@ function Connection:delete(table_name, where_clause, returning, ...) end 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") end @@ -298,26 +247,24 @@ function Connection:select(table_name, columns, where_clause, ...) columns = table.concat(columns, ", ") end - local query - if where_clause and not string.is_blank(where_clause) then - -- Handle WHERE clause parameters + if where_clause and not where_clause:is_blank() then local where_args = {...} local values = {} - local where_clause_with_params = where_clause + local where_clause_final = where_clause + for i = 1, #where_args do - table.insert(values, where_args[i]) - where_clause_with_params = string.replace(where_clause_with_params, "?", - string.template("$${num}", {num = tostring(i)}), 1) + values[i] = where_args[i] + where_clause_final = where_clause_final:replace("?", "$" .. i, 1) end - query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", { + local query = "SELECT {{columns}} FROM {{table}} WHERE {{where}}":parse({ columns = columns, table = table_name, - where = where_clause_with_params + where = where_clause_final }) return self:query(query, unpack(values)) else - query = string.template("SELECT ${columns} FROM ${table}", { + local query = "SELECT {{columns}} FROM {{table}}":parse({ columns = columns, table = table_name }) @@ -325,163 +272,137 @@ function Connection:select(table_name, columns, where_clause, ...) end end --- Enhanced PostgreSQL schema helpers +-- Schema helpers function Connection:table_exists(table_name, schema_name) - if string.is_blank(table_name) then - return false - end - + if table_name:is_blank() then return false end schema_name = schema_name or "public" - local result = self:query_value( - "SELECT tablename FROM pg_tables WHERE schemaname = $1 AND tablename = $2", - string.trim(schema_name), string.trim(table_name) - ) - return result ~= nil + return self:query_value("SELECT tablename FROM pg_tables WHERE schemaname = $1 AND tablename = $2", + schema_name:trim(), table_name:trim()) ~= nil end function Connection:column_exists(table_name, column_name, schema_name) - if string.is_blank(table_name) or string.is_blank(column_name) then - return false - end - + if table_name:is_blank() or column_name:is_blank() then return false end schema_name = schema_name or "public" - local result = self:query_value([[ + return self:query_value([[ SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3 - ]], string.trim(schema_name), string.trim(table_name), string.trim(column_name)) - return result ~= nil + ]], schema_name:trim(), table_name:trim(), column_name:trim()) ~= nil end 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") end - - local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", { + return self:exec("CREATE TABLE IF NOT EXISTS {{table}} ({{schema}})":parse({ table = table_name, - schema = string.trim(schema) - }) - return self:exec(query) + schema = schema:trim() + })) end 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") end - 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, cascade = cascade_clause - }) - return self:exec(query) + })) end 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") end - - local query = string.template("ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column}", { + return self:exec("ALTER TABLE {{table}} ADD COLUMN IF NOT EXISTS {{column}}":parse({ table = table_name, - column = string.trim(column_def) - }) - return self:exec(query) + column = column_def:trim() + })) end 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") end - 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, column = column_name, cascade = cascade_clause - }) - return self:exec(query) + })) end 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") end 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 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, index = index_name, table = table_name, method = method_clause, columns = columns_str - }) - return self:exec(query) + })) end 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") end - 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, cascade = cascade_clause - }) - return self:exec(query) + })) end -- PostgreSQL-specific functions function Connection:vacuum(table_name, analyze) local analyze_clause = analyze and " ANALYZE" or "" - local table_clause = table_name and string.template(" ${table}", {table = table_name}) or "" - return self:exec(string.template("VACUUM${analyze}${table}", { + local table_clause = table_name and " " .. table_name or "" + return self:exec("VACUUM{{analyze}}{{table}}":parse({ analyze = analyze_clause, table = table_clause })) end function Connection:analyze(table_name) - local table_clause = table_name and string.template(" ${table}", {table = table_name}) or "" - return self:exec(string.template("ANALYZE${table}", {table = table_clause})) + local table_clause = table_name and " " .. table_name or "" + return self:exec("ANALYZE{{table}}":parse({table = table_clause})) end function Connection:reindex(name, type) - if string.is_blank(name) then + if name:is_blank() then error("Name cannot be empty for REINDEX") end - type = type or "INDEX" + type = (type or "INDEX"):upper() local valid_types = {"INDEX", "TABLE", "SCHEMA", "DATABASE", "SYSTEM"} - local type_upper = string.upper(type) - if not table.contains(valid_types, type_upper) then - error(string.template("Invalid REINDEX type: ${type}", {type = type})) + if not table.contains(valid_types, type) then + error("Invalid REINDEX type: " .. type) end - return self:exec(string.template("REINDEX ${type} ${name}", { - type = type_upper, - name = name - })) + return self:exec("REINDEX {{type}} {{name}}":parse({type = type, name = name})) end --- PostgreSQL settings and introspection function Connection:show(setting) - if string.is_blank(setting) then + if setting:is_blank() then error("Setting name cannot be empty") end - return self:query_value(string.template("SHOW ${setting}", {setting = setting})) + return self:query_value("SHOW {{setting}}":parse({setting = setting})) end function Connection:set(setting, value) - if string.is_blank(setting) then + if setting:is_blank() then error("Setting name cannot be empty") end - return self:exec(string.template("SET ${setting} = ${value}", { + return self:exec("SET {{setting}} = {{value}}":parse({ setting = setting, value = tostring(value) })) @@ -505,12 +426,11 @@ end function Connection:list_tables(schema_name) schema_name = schema_name or "public" - return self:query("SELECT tablename FROM pg_tables WHERE schemaname = $1 ORDER BY tablename", - string.trim(schema_name)) + return self:query("SELECT tablename FROM pg_tables WHERE schemaname = $1 ORDER BY tablename", schema_name:trim()) end 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") end @@ -520,64 +440,64 @@ function Connection:describe_table(table_name, schema_name) FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position - ]], string.trim(schema_name), string.trim(table_name)) + ]], schema_name:trim(), table_name:trim()) end -- JSON/JSONB helpers 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") end - return string.template("${column}->'${path}'", {column = column, path = path}) + return "{{column}}->'{{path}}'":parse({column = column, path = path}) end 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") end - return string.template("${column}->>'${path}'", {column = column, path = path}) + return "{{column}}->>'{{path}}'":parse({column = column, path = path}) end 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") end - return string.template("${column} @> '${value}'", {column = column, value = value}) + return "{{column}} @> '{{value}}'":parse({column = column, value = value}) end 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") end - return string.template("${column} <@ '${value}'", {column = column, value = value}) + return "{{column}} <@ '{{value}}'":parse({column = column, value = value}) end -- Array helpers function Connection:array_contains(column, value) - if string.is_blank(column) then + if column:is_blank() then error("Column cannot be empty") end - return string.template("$1 = ANY(${column})", {column = column}) + return "$1 = ANY({{column}})":parse({column = column}) end function Connection:array_length(column) - if string.is_blank(column) then + if column:is_blank() then error("Column cannot be empty") end - return string.template("array_length(${column}, 1)", {column = column}) + return "array_length({{column}}, 1)":parse({column = column}) end -- Connection management function postgres.parse_dsn(dsn) - if string.is_blank(dsn) then + if dsn:is_blank() then return nil, "DSN cannot be empty" end local parts = {} - for pair in string.trim(dsn):gmatch("[^%s]+") do + for pair in dsn:trim():gmatch("[^%s]+") do local key, value = pair:match("([^=]+)=(.+)") if key and value then - parts[string.trim(key)] = string.trim(value) + parts[key:trim()] = value:trim() end end @@ -585,15 +505,13 @@ function postgres.parse_dsn(dsn) end function postgres.connect(dsn) - if string.is_blank(dsn) then + if dsn:is_blank() then error("DSN cannot be empty") end - local conn_id = moonshark.sql_connect("postgres", string.trim(dsn)) + local conn_id = moonshark.sql_connect("postgres", dsn:trim()) if conn_id then - local conn = {_id = conn_id} - setmetatable(conn, Connection) - return conn + return setmetatable({_id = conn_id}, Connection) end return nil end @@ -625,10 +543,7 @@ end function postgres.query_row(dsn, query_str, ...) local results = postgres.query(dsn, query_str, ...) - if results and #results > 0 then - return results[1] - end - return nil + return results and #results > 0 and results[1] or nil end 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") end - conn:create_table("_migrations", - "id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW()") + conn:create_table("_migrations", "id SERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL, applied_at TIMESTAMPTZ DEFAULT NOW()") local tx = conn:begin() if not tx then @@ -658,18 +572,14 @@ function postgres.migrate(dsn, migrations, schema) error("Failed to begin migration transaction") end - local success = true - local error_msg = "" - for _, migration in ipairs(migrations) do - if not migration.name or string.is_blank(migration.name) then - error_msg = "Migration must have a non-empty name" - success = false - break + if not migration.name or migration.name:is_blank() then + tx:rollback() + conn:close() + error("Migration must have a non-empty name") end - local existing = conn:query_value("SELECT id FROM _migrations WHERE name = $1", - string.trim(migration.name)) + local existing = conn:query_value("SELECT id FROM _migrations WHERE name = $1", migration.name:trim()) if not existing then local ok, err = pcall(function() if type(migration.up) == "string" then @@ -682,52 +592,34 @@ function postgres.migrate(dsn, migrations, schema) end) if ok then - conn:exec("INSERT INTO _migrations (name) VALUES ($1)", string.trim(migration.name)) - print(string.template("Applied migration: ${name}", {name = migration.name})) + conn:exec("INSERT INTO _migrations (name) VALUES ($1)", migration.name:trim()) + print("Applied migration: {{name}}":parse({name = migration.name})) else - success = false - error_msg = string.template("Migration '${name}' failed: ${error}", { + tx:rollback() + conn:close() + error("Migration '{{name}}' failed: {{error}}":parse({ name = migration.name, error = err or "unknown error" - }) - break + })) end end end - if success then - tx:commit() - else - tx:rollback() - conn:close() - error(error_msg) - end - + tx:commit() conn:close() return true end --- Simplified result processing utilities +-- Result processing utilities function postgres.to_array(results, column_name) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(column_name) then - error("Column name cannot be empty") - end - + if not results or table.is_empty(results) then return {} end + if column_name:is_blank() then error("Column name cannot be empty") end return table.map(results, function(row) return row[column_name] end) end function postgres.to_map(results, key_column, value_column) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(key_column) then - error("Key column name cannot be empty") - end + if not results or table.is_empty(results) then return {} end + if key_column:is_blank() then error("Key column name cannot be empty") end local map = {} for _, row in ipairs(results) do @@ -738,18 +630,11 @@ function postgres.to_map(results, key_column, value_column) end function postgres.group_by(results, column_name) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(column_name) then - error("Column name cannot be empty") - end - + if not results or table.is_empty(results) then return {} end + if column_name:is_blank() then error("Column name cannot be empty") end return table.group_by(results, function(row) return row[column_name] end) end --- Simplified debug helper function postgres.print_results(results) if not results or table.is_empty(results) then print("No results") @@ -762,18 +647,15 @@ function postgres.print_results(results) -- Calculate column widths local widths = {} for _, col in ipairs(columns) do - widths[col] = string.length(col) - end - - for _, row in ipairs(results) do - for _, col in ipairs(columns) do + widths[col] = col:length() + for _, row in ipairs(results) do 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 - -- Print header - local header_parts = table.map(columns, function(col) return string.pad_right(col, widths[col]) end) + -- Print header and separator + 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) print(table.concat(header_parts, " | ")) @@ -783,22 +665,22 @@ function postgres.print_results(results) for _, row in ipairs(results) do local value_parts = table.map(columns, function(col) local value = tostring(row[col] or "") - return string.pad_right(value, widths[col]) + return value:pad_right(widths[col]) end) print(table.concat(value_parts, " | ")) end end function postgres.escape_identifier(name) - if string.is_blank(name) then + if name:is_blank() then error("Identifier name cannot be empty") end - return string.template('"${name}"', {name = string.replace(name, '"', '""')}) + return '"{{name}}"':parse({name = name:replace('"', '""')}) end function postgres.escape_literal(value) if type(value) == "string" then - return string.template("'${value}'", {value = string.replace(value, "'", "''")}) + return "'{{value}}'":parse({value = value:replace("'", "''")}) end return tostring(value) end diff --git a/modules/sqlite/sqlite.lua b/modules/sqlite/sqlite.lua index 154867f..94aac27 100644 --- a/modules/sqlite/sqlite.lua +++ b/modules/sqlite/sqlite.lua @@ -23,24 +23,19 @@ function Connection:query(query_str, ...) if not self._id then error("Connection is closed") end - query_str = string.normalize_whitespace(query_str) - return moonshark.sql_query(self._id, query_str, ...) + return moonshark.sql_query(self._id, query_str:normalize_whitespace(), ...) end function Connection:exec(query_str, ...) if not self._id then error("Connection is closed") end - query_str = string.normalize_whitespace(query_str) - return moonshark.sql_exec(self._id, query_str, ...) + return moonshark.sql_exec(self._id, query_str:normalize_whitespace(), ...) end function Connection:query_row(query_str, ...) local results = self:query(query_str, ...) - if results and #results > 0 then - return results[1] - end - return nil + return results and #results > 0 and results[1] or nil end function Connection:query_value(query_str, ...) @@ -53,57 +48,40 @@ function Connection:query_value(query_str, ...) return nil end --- Enhanced transaction support function Connection:begin() local result = self:exec("BEGIN") if result then return { conn = self, active = true, - commit = function(tx) if tx.active then - local result = tx.conn:exec("COMMIT") tx.active = false - return result + return tx.conn:exec("COMMIT") end return false end, - rollback = function(tx) if tx.active then - local result = tx.conn:exec("ROLLBACK") tx.active = false - return result + return tx.conn:exec("ROLLBACK") end return false end, - query = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query(query_str, ...) end, - exec = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:exec(query_str, ...) end, - query_row = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query_row(query_str, ...) end, - query_value = function(tx, query_str, ...) - if not tx.active then - error("Transaction is not active") - end + if not tx.active then error("Transaction is not active") end return tx.conn:query_value(query_str, ...) end } @@ -111,93 +89,85 @@ function Connection:begin() return nil end --- Simplified query builders using table utilities 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") end local keys = table.keys(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, - columns = table.concat(keys, ", "), - placeholders = table.concat(placeholders, ", ") + columns = keys:join(", "), + placeholders = placeholders }) return self:exec(query, unpack(values)) end 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") end local keys = table.keys(data) local values = table.values(data) - local placeholders = table.map(keys, function() return "?" end) - local updates = table.map(keys, function(key) - return string.template("${key} = excluded.${key}", {key = key}) - end) + local placeholders = string.repeat_("?, ", #keys):trim_right(", ") + local updates = table.map(keys, function(key) return key .. " = excluded." .. key end):join(", ") local conflict_clause = "" if conflict_columns then if type(conflict_columns) == "string" then - conflict_clause = string.template("(${columns})", {columns = conflict_columns}) + conflict_clause = "(" .. conflict_columns .. ")" else - conflict_clause = string.template("(${columns})", {columns = table.concat(conflict_columns, ", ")}) + conflict_clause = "(" .. table.concat(conflict_columns, ", ") .. ")" 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, - columns = table.concat(keys, ", "), - placeholders = table.concat(placeholders, ", "), + columns = keys:join(", "), + placeholders = placeholders, conflict = conflict_clause, - updates = table.concat(updates, ", ") + updates = updates }) return self:exec(query, unpack(values)) end 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") end - if string.is_blank(where_clause) then + if where_clause:is_blank() then error("WHERE clause cannot be empty for UPDATE") end local keys = table.keys(data) local values = table.values(data) - local sets = table.map(keys, function(key) - return string.template("${key} = ?", {key = key}) - end) + local sets = table.map(keys, function(key) return key .. " = ?" end):join(", ") - local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", { + local query = "UPDATE {{table}} SET {{sets}} WHERE {{where}}":parse({ table = table_name, - sets = table.concat(sets, ", "), + sets = sets, where = where_clause }) - -- Add WHERE clause parameters - local where_args = {...} - table.extend(values, where_args) - + table.extend(values, {...}) return self:exec(query, unpack(values)) end 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") end - if string.is_blank(where_clause) then + if where_clause:is_blank() then error("WHERE clause cannot be empty for DELETE") end - local query = string.template("DELETE FROM ${table} WHERE ${where}", { + local query = "DELETE FROM {{table}} WHERE {{where}}":parse({ table = table_name, where = where_clause }) @@ -205,7 +175,7 @@ function Connection:delete(table_name, where_clause, ...) end 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") end @@ -214,16 +184,15 @@ function Connection:select(table_name, columns, where_clause, ...) columns = table.concat(columns, ", ") end - local query - if where_clause and not string.is_blank(where_clause) then - query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", { + if where_clause and not where_clause:is_blank() then + local query = "SELECT {{columns}} FROM {{table}} WHERE {{where}}":parse({ columns = columns, table = table_name, where = where_clause }) return self:query(query, ...) else - query = string.template("SELECT ${columns} FROM ${table}", { + local query = "SELECT {{columns}} FROM {{table}}":parse({ columns = columns, table = table_name }) @@ -231,75 +200,63 @@ function Connection:select(table_name, columns, where_clause, ...) end end --- Schema helpers function Connection:table_exists(table_name) - if string.is_blank(table_name) then - return false - end - - local result = self:query_value( - "SELECT name FROM sqlite_master WHERE type='table' AND name=?", - string.trim(table_name) - ) - return result ~= nil + if table_name:is_blank() then return false end + return self:query_value("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table_name:trim()) ~= nil end function Connection:column_exists(table_name, column_name) - if string.is_blank(table_name) or string.is_blank(column_name) then - return false - end + if table_name:is_blank() or column_name:is_blank() then 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 return table.any(result, function(row) - return string.iequals(row.name, string.trim(column_name)) + return row.name:iequals(column_name:trim()) end) end return false end 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") 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, - schema = string.trim(schema) + schema = schema:trim() }) return self:exec(query) end 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") end - - local query = string.template("DROP TABLE IF EXISTS ${table}", {table = table_name}) - return self:exec(query) + return self:exec("DROP TABLE IF EXISTS {{table}}":parse({table = table_name})) end 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") end - local query = string.template("ALTER TABLE ${table} ADD COLUMN ${column}", { + local query = "ALTER TABLE {{table}} ADD COLUMN {{column}}":parse({ table = table_name, - column = string.trim(column_def) + column = column_def:trim() }) return self:exec(query) end 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") end local unique_clause = unique and "UNIQUE " or "" 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, index = index_name, table = table_name, @@ -309,12 +266,10 @@ function Connection:create_index(index_name, table_name, columns, unique) end 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") end - - local query = string.template("DROP INDEX IF EXISTS ${index}", {index = index_name}) - return self:exec(query) + return self:exec("DROP INDEX IF EXISTS {{index}}":parse({index = index_name})) end -- SQLite-specific functions @@ -332,29 +287,29 @@ end function Connection:foreign_keys(enabled) 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 function Connection:journal_mode(mode) - mode = mode or "WAL" + mode = (mode or "WAL"):upper() 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) 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 function Connection:synchronous(level) - level = level or "NORMAL" + level = (level or "NORMAL"):upper() 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) end - return self:exec(string.template("PRAGMA synchronous = ${level}", {level = string.upper(level)})) + return self:exec("PRAGMA synchronous = {{level}}":parse({level = level})) end function Connection:cache_size(size) @@ -362,35 +317,30 @@ function Connection:cache_size(size) if type(size) ~= "number" then error("Cache size must be a number") 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 function Connection:temp_store(mode) - mode = mode or "MEMORY" + mode = (mode or "MEMORY"):upper() 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) 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 -- Connection management function sqlite.open(database_path) database_path = database_path or ":memory:" - - if database_path ~= ":memory:" then - if string.is_blank(database_path) then - database_path = ":memory:" - end + if database_path ~= ":memory:" and database_path:is_blank() then + database_path = ":memory:" end local conn_id = moonshark.sql_connect("sqlite", database_path:trim()) if conn_id then - local conn = {_id = conn_id} - setmetatable(conn, Connection) - return conn + return setmetatable({_id = conn_id}, Connection) end return nil end @@ -401,9 +351,7 @@ sqlite.connect = sqlite.open function sqlite.query(database_path, query_str, ...) local conn = sqlite.open(database_path) if not conn then - error(string.template("Failed to open SQLite database: ${path}", { - path = database_path or ":memory:" - })) + error("Failed to open SQLite database: {{path}}":parse({path = database_path or ":memory:"})) end local results = conn:query(query_str, ...) @@ -414,9 +362,7 @@ end function sqlite.exec(database_path, query_str, ...) local conn = sqlite.open(database_path) if not conn then - error(string.template("Failed to open SQLite database: ${path}", { - path = database_path or ":memory:" - })) + error("Failed to open SQLite database: {{path}}":parse({path = database_path or ":memory:"})) end local result = conn:exec(query_str, ...) @@ -426,10 +372,7 @@ end function sqlite.query_row(database_path, query_str, ...) local results = sqlite.query(database_path, query_str, ...) - if results and #results > 0 then - return results[1] - end - return nil + return results and #results > 0 and results[1] or nil end 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") end - conn:create_table("_migrations", - "id INTEGER PRIMARY KEY, name TEXT UNIQUE, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP") + conn:create_table("_migrations", "id INTEGER PRIMARY KEY, name TEXT UNIQUE, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP") local tx = conn:begin() if not tx then @@ -458,18 +400,14 @@ function sqlite.migrate(database_path, migrations) error("Failed to begin migration transaction") end - local success = true - local error_msg = "" - for _, migration in ipairs(migrations) do - if not migration.name or string.is_blank(migration.name) then - error_msg = "Migration must have a non-empty name" - success = false - break + if not migration.name or migration.name:is_blank() then + tx:rollback() + conn:close() + error("Migration must have a non-empty name") end - local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?", - string.trim(migration.name)) + local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?", migration.name:trim()) if not existing then local ok, err = pcall(function() if type(migration.up) == "string" then @@ -482,52 +420,34 @@ function sqlite.migrate(database_path, migrations) end) if ok then - conn:exec("INSERT INTO _migrations (name) VALUES (?)", string.trim(migration.name)) - print(string.template("Applied migration: ${name}", {name = migration.name})) + conn:exec("INSERT INTO _migrations (name) VALUES (?)", migration.name:trim()) + print("Applied migration: {{name}}":parse({name = migration.name})) else - success = false - error_msg = string.template("Migration '${name}' failed: ${error}", { + tx:rollback() + conn:close() + error("Migration '{{name}}' failed: {{error}}":parse({ name = migration.name, error = err or "unknown error" - }) - break + })) end end end - if success then - tx:commit() - else - tx:rollback() - conn:close() - error(error_msg) - end - + tx:commit() conn:close() return true end --- Simplified result processing using table utilities +-- Result processing utilities function sqlite.to_array(results, column_name) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(column_name) then - error("Column name cannot be empty") - end - + if not results or table.is_empty(results) then return {} end + if column_name:is_blank() then error("Column name cannot be empty") end return table.map(results, function(row) return row[column_name] end) end function sqlite.to_map(results, key_column, value_column) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(key_column) then - error("Key column name cannot be empty") - end + if not results or table.is_empty(results) then return {} end + if key_column:is_blank() then error("Key column name cannot be empty") end local map = {} for _, row in ipairs(results) do @@ -538,18 +458,11 @@ function sqlite.to_map(results, key_column, value_column) end function sqlite.group_by(results, column_name) - if not results or table.is_empty(results) then - return {} - end - - if string.is_blank(column_name) then - error("Column name cannot be empty") - end - + if not results or table.is_empty(results) then return {} end + if column_name:is_blank() then error("Column name cannot be empty") end return table.group_by(results, function(row) return row[column_name] end) end --- Simplified debug helper function sqlite.print_results(results) if not results or table.is_empty(results) then print("No results") @@ -560,17 +473,17 @@ function sqlite.print_results(results) table.sort(columns) -- 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) - - for _, row in ipairs(results) do - for _, col in ipairs(columns) do + local widths = {} + for _, col in ipairs(columns) do + widths[col] = col:length() + for _, row in ipairs(results) do 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 - -- Print header - local header_parts = table.map(columns, function(col) return string.pad_right(col, widths[col]) end) + -- Print header and separator + 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) print(table.concat(header_parts, " | ")) @@ -580,7 +493,7 @@ function sqlite.print_results(results) for _, row in ipairs(results) do local value_parts = table.map(columns, function(col) local value = tostring(row[col] or "") - return string.pad_right(value, widths[col]) + return value:pad_right(widths[col]) end) print(table.concat(value_parts, " | ")) end