remove string requires, fix calls

This commit is contained in:
Sky Johnson 2025-07-24 16:48:29 -05:00
parent 41cba2f049
commit 2d43c457e1
4 changed files with 346 additions and 350 deletions

View File

@ -1,4 +1,3 @@
local str = require("string")
local json = require("json")
local http = {}
@ -19,29 +18,29 @@ Response.__index = Response
local function parse_cookies(cookie_header)
local cookies = {}
if str.is_empty(cookie_header) then
if string.is_empty(cookie_header) then
return cookies
end
-- Split by semicolon and parse each cookie
local cookie_pairs = str.split(cookie_header, ";")
local cookie_pairs = string.split(cookie_header, ";")
for _, cookie_pair in ipairs(cookie_pairs) do
local trimmed = str.trim(cookie_pair)
if not str.is_empty(trimmed) then
local parts = str.split(trimmed, "=")
local trimmed = string.trim(cookie_pair)
if not string.is_empty(trimmed) then
local parts = string.split(trimmed, "=")
if #parts >= 2 then
local name = str.trim(parts[1])
local value = str.trim(parts[2])
local name = string.trim(parts[1])
local value = string.trim(parts[2])
-- URL decode the value
local success, decoded = pcall(function()
return str.url_decode(value)
return string.url_decode(value)
end)
cookies[name] = success and decoded or value
elseif #parts == 1 then
-- Cookie without value
cookies[str.trim(parts[1])] = ""
cookies[string.trim(parts[1])] = ""
end
end
end
@ -54,17 +53,17 @@ end
-- ======================================================================
local function split_path(path)
if str.is_empty(path) or path == "/" then
if string.is_empty(path) or path == "/" then
return {}
end
-- Remove leading/trailing slashes and split
local clean_path = str.trim(path, "/")
if str.is_empty(clean_path) then
local clean_path = string.trim(path, "/")
if string.is_empty(clean_path) then
return {}
end
return str.split(clean_path, "/")
return string.split(clean_path, "/")
end
local function match_route(method, path)
@ -86,12 +85,12 @@ local function match_route(method, path)
for j = i, #path_segments do
table.insert(remaining, path_segments[j])
end
params["*"] = str.join(remaining, "/")
params["*"] = string.join(remaining, "/")
break
elseif str.starts_with(route_seg, ":") then
elseif string.starts_with(route_seg, ":") then
-- Parameter segment
if i <= #path_segments then
local param_name = str.slice(route_seg, 2, -1)
local param_name = string.slice(route_seg, 2, -1)
params[param_name] = path_segments[i]
else
match = false
@ -135,7 +134,7 @@ function _http_handle_request(req_table, res_table)
end
local mw = _G._http_middleware[index]
if mw.path == nil or str.starts_with(req.path, mw.path) then
if mw.path == nil or string.starts_with(req.path, mw.path) then
mw.handler(req, res, function()
run_middleware(index + 1)
end)
@ -203,7 +202,7 @@ end
function Server:_add_route(method, path, handler)
-- Ensure path starts with /
if not str.starts_with(path, "/") then
if not string.starts_with(path, "/") then
path = "/" .. path
end
@ -321,7 +320,7 @@ function Request.new(req_table)
end
function Request:get(header_name)
local lower_name = str.lower(header_name)
local lower_name = string.lower(header_name)
return self.headers[header_name] or self.headers[lower_name]
end
@ -359,7 +358,7 @@ function Request:cookie_matches(name, pattern)
if not cookie_value then
return false
end
return str.match(pattern, cookie_value)
return string.match(pattern, cookie_value)
end
function Request:get_cookies_by_names(names)
@ -381,7 +380,7 @@ function Request:has_auth_cookies()
end
function Request:json()
if str.is_empty(self.body) then
if string.is_empty(self.body) then
return nil
end
@ -398,27 +397,27 @@ end
function Request:is_json()
local content_type = self:get("content-type") or ""
return str.contains(content_type, "application/json")
return string.contains(content_type, "application/json")
end
function Request:is_form()
local content_type = self:get("content-type") or ""
return str.contains(content_type, "application/x-www-form-urlencoded")
return string.contains(content_type, "application/x-www-form-urlencoded")
end
function Request:is_multipart()
local content_type = self:get("content-type") or ""
return str.contains(content_type, "multipart/form-data")
return string.contains(content_type, "multipart/form-data")
end
function Request:is_xml()
local content_type = self:get("content-type") or ""
return str.contains(content_type, "application/xml") or str.contains(content_type, "text/xml")
return string.contains(content_type, "application/xml") or string.contains(content_type, "text/xml")
end
function Request:accepts(mime_type)
local accept_header = self:get("accept") or ""
return str.contains(accept_header, mime_type) or str.contains(accept_header, "*/*")
return string.contains(accept_header, mime_type) or string.contains(accept_header, "*/*")
end
function Request:user_agent()
@ -431,7 +430,7 @@ end
function Request:is_secure()
local proto = self:get("x-forwarded-proto")
return proto == "https" or str.starts_with(self:get("host") or "", "https://")
return proto == "https" or string.starts_with(self:get("host") or "", "https://")
end
-- ======================================================================
@ -565,8 +564,8 @@ function Response:cookie(name, value, options)
local cookie_value = tostring(value)
-- URL encode the cookie value if it contains special characters
if str.match("[;,\\s]", cookie_value) then
cookie_value = str.url_encode(cookie_value)
if string.match("[;,\\s]", cookie_value) then
cookie_value = string.url_encode(cookie_value)
end
local cookie = name .. "=" .. cookie_value
@ -638,13 +637,13 @@ function Response:download(data, filename, content_type)
self:type(content_type)
elseif filename then
-- Try to guess content type from extension
if str.ends_with(filename, ".pdf") then
if string.ends_with(filename, ".pdf") then
self:type("application/pdf")
elseif str.ends_with(filename, ".zip") then
elseif string.ends_with(filename, ".zip") then
self:type("application/zip")
elseif str.ends_with(filename, ".json") then
elseif string.ends_with(filename, ".json") then
self:type("application/json")
elseif str.ends_with(filename, ".csv") then
elseif string.ends_with(filename, ".csv") then
self:type("text/csv")
else
self:type("application/octet-stream")
@ -675,7 +674,7 @@ function http.cors(options)
res:header("Access-Control-Allow-Credentials", "true")
end
if str.iequals(req.method, "OPTIONS") then
if string.iequals(req.method, "OPTIONS") then
res:status(204):send("")
else
next()
@ -687,7 +686,7 @@ function http.static(root_path, url_prefix)
url_prefix = url_prefix or "/"
-- Ensure prefix starts with /
if not str.starts_with(url_prefix, "/") then
if not string.starts_with(url_prefix, "/") then
url_prefix = "/" .. url_prefix
end
@ -706,7 +705,7 @@ end
function http.json_parser()
return function(req, res, next)
if req:is_json() and not str.is_empty(req.body) then
if req:is_json() and not string.is_empty(req.body) then
local success, data = pcall(function()
return req:json()
end)
@ -733,11 +732,11 @@ function http.logger(format)
local duration = (os.clock() - start_time) * 1000
local status = res._table.status or 200
local log_message = str.template(format, {
local log_message = string.template(format, {
method = req.method,
path = req.path,
status = status,
["response-time"] = str.format("%.2f", duration),
["response-time"] = string.format("%.2f", duration),
["user-agent"] = req:user_agent(),
ip = req:ip()
})
@ -749,9 +748,9 @@ end
function http.compression()
return function(req, res, next)
local accept_encoding = req:get("accept-encoding") or ""
if str.contains(accept_encoding, "gzip") then
if string.contains(accept_encoding, "gzip") then
res:header("Content-Encoding", "gzip")
elseif str.contains(accept_encoding, "deflate") then
elseif string.contains(accept_encoding, "deflate") then
res:header("Content-Encoding", "deflate")
end
next()

View File

@ -1,4 +1,3 @@
local str = require("string")
local tbl = require("table")
local mysql = {}
@ -25,7 +24,7 @@ function Connection:query(query_str, ...)
if not self._id then
error("Connection is closed")
end
query_str = str.normalize_whitespace(query_str)
query_str = string.normalize_whitespace(query_str)
return moonshark.sql_query(self._id, query_str, ...)
end
@ -33,7 +32,7 @@ function Connection:exec(query_str, ...)
if not self._id then
error("Connection is closed")
end
query_str = str.normalize_whitespace(query_str)
query_str = string.normalize_whitespace(query_str)
return moonshark.sql_exec(self._id, query_str, ...)
end
@ -85,20 +84,20 @@ function Connection:begin()
if not tx.active then
error("Transaction is not active")
end
if str.is_blank(name) then
if string.is_blank(name) then
error("Savepoint name cannot be empty")
end
return tx.conn:exec(str.template("SAVEPOINT ${name}", {name = name}))
return tx.conn:exec(string.template("SAVEPOINT ${name}", {name = name}))
end,
rollback_to = function(tx, name)
if not tx.active then
error("Transaction is not active")
end
if str.is_blank(name) then
if string.is_blank(name) then
error("Savepoint name cannot be empty")
end
return tx.conn:exec(str.template("ROLLBACK TO SAVEPOINT ${name}", {name = name}))
return tx.conn:exec(string.template("ROLLBACK TO SAVEPOINT ${name}", {name = name}))
end,
query = function(tx, query_str, ...)
@ -135,7 +134,7 @@ end
-- Simplified MySQL-specific query builder helpers
function Connection:insert(table_name, data)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -143,7 +142,7 @@ function Connection:insert(table_name, data)
local values = tbl.values(data)
local placeholders = tbl.map(keys, function() return "?" end)
local query = str.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", ")
@ -153,7 +152,7 @@ function Connection:insert(table_name, data)
end
function Connection:upsert(table_name, data, update_data)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -164,10 +163,10 @@ function Connection:upsert(table_name, data, update_data)
-- Use update_data if provided, otherwise update with same data
local update_source = update_data or data
local updates = tbl.map(tbl.keys(update_source), function(key)
return str.template("${key} = VALUES(${key})", {key = key})
return string.template("${key} = VALUES(${key})", {key = key})
end)
local query = str.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}", {
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updates}", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", "),
@ -178,7 +177,7 @@ function Connection:upsert(table_name, data, update_data)
end
function Connection:replace(table_name, data)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -186,7 +185,7 @@ function Connection:replace(table_name, data)
local values = tbl.values(data)
local placeholders = tbl.map(keys, function() return "?" end)
local query = str.template("REPLACE INTO ${table} (${columns}) VALUES (${placeholders})", {
local query = string.template("REPLACE INTO ${table} (${columns}) VALUES (${placeholders})", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", ")
@ -196,20 +195,20 @@ function Connection:replace(table_name, data)
end
function Connection:update(table_name, data, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
if str.is_blank(where_clause) then
if string.is_blank(where_clause) then
error("WHERE clause cannot be empty for UPDATE")
end
local keys = tbl.keys(data)
local values = tbl.values(data)
local sets = tbl.map(keys, function(key)
return str.template("${key} = ?", {key = key})
return string.template("${key} = ?", {key = key})
end)
local query = str.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
table = table_name,
sets = tbl.concat(sets, ", "),
where = where_clause
@ -223,14 +222,14 @@ function Connection:update(table_name, data, where_clause, ...)
end
function Connection:delete(table_name, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
if str.is_blank(where_clause) then
if string.is_blank(where_clause) then
error("WHERE clause cannot be empty for DELETE")
end
local query = str.template("DELETE FROM ${table} WHERE ${where}", {
local query = string.template("DELETE FROM ${table} WHERE ${where}", {
table = table_name,
where = where_clause
})
@ -238,7 +237,7 @@ function Connection:delete(table_name, where_clause, ...)
end
function Connection:select(table_name, columns, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -248,15 +247,15 @@ function Connection:select(table_name, columns, where_clause, ...)
end
local query
if where_clause and not str.is_blank(where_clause) then
query = str.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
if where_clause and not string.is_blank(where_clause) then
query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
columns = columns,
table = table_name,
where = where_clause
})
return self:query(query, ...)
else
query = str.template("SELECT ${columns} FROM ${table}", {
query = string.template("SELECT ${columns} FROM ${table}", {
columns = columns,
table = table_name
})
@ -266,19 +265,19 @@ end
-- MySQL schema helpers
function Connection:database_exists(database_name)
if str.is_blank(database_name) then
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 = ?",
str.trim(database_name)
string.trim(database_name)
)
return result ~= nil
end
function Connection:table_exists(table_name, database_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
return false
end
@ -289,13 +288,13 @@ function Connection:table_exists(table_name, database_name)
local result = self:query_value(
"SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
str.trim(database_name), str.trim(table_name)
string.trim(database_name), string.trim(table_name)
)
return result ~= nil
end
function Connection:column_exists(table_name, column_name, database_name)
if str.is_blank(table_name) or str.is_blank(column_name) then
if string.is_blank(table_name) or string.is_blank(column_name) then
return false
end
@ -307,19 +306,19 @@ function Connection:column_exists(table_name, column_name, database_name)
local result = self:query_value([[
SELECT COLUMN_NAME FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?
]], str.trim(database_name), str.trim(table_name), str.trim(column_name))
]], string.trim(database_name), string.trim(table_name), string.trim(column_name))
return result ~= nil
end
function Connection:create_database(database_name, charset, collation)
if str.is_blank(database_name) then
if string.is_blank(database_name) then
error("Database name cannot be empty")
end
local charset_clause = charset and str.template(" CHARACTER SET ${charset}", {charset = charset}) or ""
local collation_clause = collation and str.template(" COLLATE ${collation}", {collation = collation}) or ""
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 query = str.template("CREATE DATABASE IF NOT EXISTS ${database}${charset}${collation}", {
local query = string.template("CREATE DATABASE IF NOT EXISTS ${database}${charset}${collation}", {
database = database_name,
charset = charset_clause,
collation = collation_clause
@ -328,25 +327,25 @@ function Connection:create_database(database_name, charset, collation)
end
function Connection:drop_database(database_name)
if str.is_blank(database_name) then
if string.is_blank(database_name) then
error("Database name cannot be empty")
end
local query = str.template("DROP DATABASE IF EXISTS ${database}", {database = database_name})
local query = string.template("DROP DATABASE IF EXISTS ${database}", {database = database_name})
return self:exec(query)
end
function Connection:create_table(table_name, schema, engine, charset)
if str.is_blank(table_name) or str.is_blank(schema) then
if string.is_blank(table_name) or string.is_blank(schema) then
error("Table name and schema cannot be empty")
end
local engine_clause = engine and str.template(" ENGINE=${engine}", {engine = str.upper(engine)}) or ""
local charset_clause = charset and str.template(" CHARACTER SET ${charset}", {charset = charset}) or ""
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 query = str.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})${engine}${charset}", {
local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})${engine}${charset}", {
table = table_name,
schema = str.trim(schema),
schema = string.trim(schema),
engine = engine_clause,
charset = charset_clause
})
@ -354,34 +353,34 @@ function Connection:create_table(table_name, schema, engine, charset)
end
function Connection:drop_table(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
local query = str.template("DROP TABLE IF EXISTS ${table}", {table = table_name})
local query = string.template("DROP TABLE IF EXISTS ${table}", {table = table_name})
return self:exec(query)
end
function Connection:add_column(table_name, column_def, position)
if str.is_blank(table_name) or str.is_blank(column_def) then
if string.is_blank(table_name) or string.is_blank(column_def) then
error("Table name and column definition cannot be empty")
end
local position_clause = position and str.template(" ${position}", {position = position}) or ""
local query = str.template("ALTER TABLE ${table} ADD COLUMN ${column}${position}", {
local position_clause = position and string.template(" ${position}", {position = position}) or ""
local query = string.template("ALTER TABLE ${table} ADD COLUMN ${column}${position}", {
table = table_name,
column = str.trim(column_def),
column = string.trim(column_def),
position = position_clause
})
return self:exec(query)
end
function Connection:drop_column(table_name, column_name)
if str.is_blank(table_name) or str.is_blank(column_name) then
if string.is_blank(table_name) or string.is_blank(column_name) then
error("Table name and column name cannot be empty")
end
local query = str.template("ALTER TABLE ${table} DROP COLUMN ${column}", {
local query = string.template("ALTER TABLE ${table} DROP COLUMN ${column}", {
table = table_name,
column = column_name
})
@ -389,23 +388,23 @@ function Connection:drop_column(table_name, column_name)
end
function Connection:modify_column(table_name, column_def)
if str.is_blank(table_name) or str.is_blank(column_def) then
if string.is_blank(table_name) or string.is_blank(column_def) then
error("Table name and column definition cannot be empty")
end
local query = str.template("ALTER TABLE ${table} MODIFY COLUMN ${column}", {
local query = string.template("ALTER TABLE ${table} MODIFY COLUMN ${column}", {
table = table_name,
column = str.trim(column_def)
column = string.trim(column_def)
})
return self:exec(query)
end
function Connection:rename_table(old_name, new_name)
if str.is_blank(old_name) or str.is_blank(new_name) then
if string.is_blank(old_name) or string.is_blank(new_name) then
error("Old and new table names cannot be empty")
end
local query = str.template("RENAME TABLE ${old} TO ${new}", {
local query = string.template("RENAME TABLE ${old} TO ${new}", {
old = old_name,
new = new_name
})
@ -413,15 +412,15 @@ function Connection:rename_table(old_name, new_name)
end
function Connection:create_index(index_name, table_name, columns, unique, type)
if str.is_blank(index_name) or str.is_blank(table_name) then
if string.is_blank(index_name) or string.is_blank(table_name) then
error("Index name and table name cannot be empty")
end
local unique_clause = unique and "UNIQUE " or ""
local type_clause = type and str.template(" USING ${type}", {type = str.upper(type)}) or ""
local type_clause = type and string.template(" USING ${type}", {type = string.upper(type)}) or ""
local columns_str = type(columns) == "table" and tbl.concat(columns, ", ") or tostring(columns)
local query = str.template("CREATE ${unique}INDEX ${index} ON ${table} (${columns})${type}", {
local query = string.template("CREATE ${unique}INDEX ${index} ON ${table} (${columns})${type}", {
unique = unique_clause,
index = index_name,
table = table_name,
@ -432,11 +431,11 @@ function Connection:create_index(index_name, table_name, columns, unique, type)
end
function Connection:drop_index(index_name, table_name)
if str.is_blank(index_name) or str.is_blank(table_name) then
if string.is_blank(index_name) or string.is_blank(table_name) then
error("Index name and table name cannot be empty")
end
local query = str.template("DROP INDEX ${index} ON ${table}", {
local query = string.template("DROP INDEX ${index} ON ${table}", {
index = index_name,
table = table_name
})
@ -445,51 +444,51 @@ end
-- MySQL maintenance functions
function Connection:optimize(table_name)
local table_clause = table_name and str.template(" ${table}", {table = table_name}) or ""
return self:query(str.template("OPTIMIZE TABLE${table}", {table = table_clause}))
local table_clause = table_name and string.template(" ${table}", {table = table_name}) or ""
return self:query(string.template("OPTIMIZE TABLE${table}", {table = table_clause}))
end
function Connection:repair(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty for REPAIR")
end
return self:query(str.template("REPAIR TABLE ${table}", {table = table_name}))
return self:query(string.template("REPAIR TABLE ${table}", {table = table_name}))
end
function Connection:check_table(table_name, options)
if str.is_blank(table_name) then
if string.is_blank(table_name) 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 = str.upper(options)
local options_upper = string.upper(options)
if tbl.contains(valid_options, options_upper) then
options_clause = str.template(" ${options}", {options = options_upper})
options_clause = string.template(" ${options}", {options = options_upper})
end
end
return self:query(str.template("CHECK TABLE ${table}${options}", {
return self:query(string.template("CHECK TABLE ${table}${options}", {
table = table_name,
options = options_clause
}))
end
function Connection:analyze_table(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty for ANALYZE")
end
return self:query(str.template("ANALYZE TABLE ${table}", {table = table_name}))
return self:query(string.template("ANALYZE TABLE ${table}", {table = table_name}))
end
-- MySQL settings and introspection
function Connection:show(what)
if str.is_blank(what) then
if string.is_blank(what) then
error("SHOW parameter cannot be empty")
end
return self:query(str.template("SHOW ${what}", {what = str.upper(what)}))
return self:query(string.template("SHOW ${what}", {what = string.upper(what)}))
end
function Connection:current_database()
@ -509,36 +508,36 @@ function Connection:list_databases()
end
function Connection:list_tables(database_name)
if database_name and not str.is_blank(database_name) then
return self:query(str.template("SHOW TABLES FROM ${database}", {database = 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}))
else
return self:query("SHOW TABLES")
end
end
function Connection:describe_table(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
return self:query(str.template("DESCRIBE ${table}", {table = table_name}))
return self:query(string.template("DESCRIBE ${table}", {table = table_name}))
end
function Connection:show_create_table(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
return self:query(str.template("SHOW CREATE TABLE ${table}", {table = table_name}))
return self:query(string.template("SHOW CREATE TABLE ${table}", {table = table_name}))
end
function Connection:show_indexes(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
return self:query(str.template("SHOW INDEXES FROM ${table}", {table = table_name}))
return self:query(string.template("SHOW INDEXES FROM ${table}", {table = table_name}))
end
function Connection:show_table_status(table_name)
if table_name and not str.is_blank(table_name) then
if table_name and not string.is_blank(table_name) then
return self:query("SHOW TABLE STATUS LIKE ?", table_name)
else
return self:query("SHOW TABLE STATUS")
@ -547,12 +546,12 @@ end
-- MySQL user and privilege management
function Connection:create_user(username, password, host)
if str.is_blank(username) or str.is_blank(password) then
if string.is_blank(username) or string.is_blank(password) then
error("Username and password cannot be empty")
end
host = host or "%"
local query = str.template("CREATE USER '${username}'@'${host}' IDENTIFIED BY ?", {
local query = string.template("CREATE USER '${username}'@'${host}' IDENTIFIED BY ?", {
username = username,
host = host
})
@ -560,12 +559,12 @@ function Connection:create_user(username, password, host)
end
function Connection:drop_user(username, host)
if str.is_blank(username) then
if string.is_blank(username) then
error("Username cannot be empty")
end
host = host or "%"
local query = str.template("DROP USER IF EXISTS '${username}'@'${host}'", {
local query = string.template("DROP USER IF EXISTS '${username}'@'${host}'", {
username = username,
host = host
})
@ -573,16 +572,16 @@ function Connection:drop_user(username, host)
end
function Connection:grant(privileges, database, table_name, username, host)
if str.is_blank(privileges) or str.is_blank(database) or str.is_blank(username) then
if string.is_blank(privileges) or string.is_blank(database) or string.is_blank(username) then
error("Privileges, database, and username cannot be empty")
end
host = host or "%"
table_name = table_name or "*"
local object = str.template("${database}.${table}", {database = database, table = table_name})
local object = string.template("${database}.${table}", {database = database, table = table_name})
local query = str.template("GRANT ${privileges} ON ${object} TO '${username}'@'${host}'", {
privileges = str.upper(privileges),
local query = string.template("GRANT ${privileges} ON ${object} TO '${username}'@'${host}'", {
privileges = string.upper(privileges),
object = object,
username = username,
host = host
@ -591,16 +590,16 @@ function Connection:grant(privileges, database, table_name, username, host)
end
function Connection:revoke(privileges, database, table_name, username, host)
if str.is_blank(privileges) or str.is_blank(database) or str.is_blank(username) then
if string.is_blank(privileges) or string.is_blank(database) or string.is_blank(username) then
error("Privileges, database, and username cannot be empty")
end
host = host or "%"
table_name = table_name or "*"
local object = str.template("${database}.${table}", {database = database, table = table_name})
local object = string.template("${database}.${table}", {database = database, table = table_name})
local query = str.template("REVOKE ${privileges} ON ${object} FROM '${username}'@'${host}'", {
privileges = str.upper(privileges),
local query = string.template("REVOKE ${privileges} ON ${object} FROM '${username}'@'${host}'", {
privileges = string.upper(privileges),
object = object,
username = username,
host = host
@ -614,31 +613,31 @@ end
-- MySQL variables and configuration
function Connection:set_variable(name, value, global)
if str.is_blank(name) then
if string.is_blank(name) then
error("Variable name cannot be empty")
end
local scope = global and "GLOBAL " or "SESSION "
return self:exec(str.template("SET ${scope}${name} = ?", {
return self:exec(string.template("SET ${scope}${name} = ?", {
scope = scope,
name = name
}), value)
end
function Connection:get_variable(name, global)
if str.is_blank(name) then
if string.is_blank(name) then
error("Variable name cannot be empty")
end
local scope = global and "global." or "session."
return self:query_value(str.template("SELECT @@${scope}${name}", {
return self:query_value(string.template("SELECT @@${scope}${name}", {
scope = scope,
name = name
}))
end
function Connection:show_variables(pattern)
if pattern and not str.is_blank(pattern) then
if pattern and not string.is_blank(pattern) then
return self:query("SHOW VARIABLES LIKE ?", pattern)
else
return self:query("SHOW VARIABLES")
@ -646,7 +645,7 @@ function Connection:show_variables(pattern)
end
function Connection:show_status(pattern)
if pattern and not str.is_blank(pattern) then
if pattern and not string.is_blank(pattern) then
return self:query("SHOW STATUS LIKE ?", pattern)
else
return self:query("SHOW STATUS")
@ -655,11 +654,11 @@ end
-- Connection management
function mysql.connect(dsn)
if str.is_blank(dsn) then
if string.is_blank(dsn) then
error("DSN cannot be empty")
end
local conn_id = moonshark.sql_connect("mysql", str.trim(dsn))
local conn_id = moonshark.sql_connect("mysql", string.trim(dsn))
if conn_id then
local conn = {_id = conn_id}
setmetatable(conn, Connection)
@ -719,8 +718,8 @@ function mysql.migrate(dsn, migrations, database_name)
end
-- Use specified database if provided
if database_name and not str.is_blank(database_name) then
conn:exec(str.template("USE ${database}", {database = database_name}))
if database_name and not string.is_blank(database_name) then
conn:exec(string.template("USE ${database}", {database = database_name}))
end
-- Create migrations table
@ -737,7 +736,7 @@ function mysql.migrate(dsn, migrations, database_name)
local error_msg = ""
for _, migration in ipairs(migrations) do
if not migration.name or str.is_blank(migration.name) then
if not migration.name or string.is_blank(migration.name) then
error_msg = "Migration must have a non-empty name"
success = false
break
@ -745,7 +744,7 @@ function mysql.migrate(dsn, migrations, database_name)
-- Check if migration already applied
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?",
str.trim(migration.name))
string.trim(migration.name))
if not existing then
local ok, err = pcall(function()
if type(migration.up) == "string" then
@ -758,11 +757,11 @@ function mysql.migrate(dsn, migrations, database_name)
end)
if ok then
conn:exec("INSERT INTO _migrations (name) VALUES (?)", str.trim(migration.name))
print(str.template("Applied migration: ${name}", {name = migration.name}))
conn:exec("INSERT INTO _migrations (name) VALUES (?)", string.trim(migration.name))
print(string.template("Applied migration: ${name}", {name = migration.name}))
else
success = false
error_msg = str.template("Migration '${name}' failed: ${error}", {
error_msg = string.template("Migration '${name}' failed: ${error}", {
name = migration.name,
error = err or "unknown error"
})
@ -789,7 +788,7 @@ function mysql.to_array(results, column_name)
return {}
end
if str.is_blank(column_name) then
if string.is_blank(column_name) then
error("Column name cannot be empty")
end
@ -801,7 +800,7 @@ function mysql.to_map(results, key_column, value_column)
return {}
end
if str.is_blank(key_column) then
if string.is_blank(key_column) then
error("Key column name cannot be empty")
end
@ -818,7 +817,7 @@ function mysql.group_by(results, column_name)
return {}
end
if str.is_blank(column_name) then
if string.is_blank(column_name) then
error("Column name cannot be empty")
end
@ -838,19 +837,19 @@ function mysql.print_results(results)
-- Calculate column widths
local widths = {}
for _, col in ipairs(columns) do
widths[col] = str.length(col)
widths[col] = string.length(col)
end
for _, row in ipairs(results) do
for _, col in ipairs(columns) do
local value = tostring(row[col] or "")
widths[col] = math.max(widths[col], str.length(value))
widths[col] = math.max(widths[col], string.length(value))
end
end
-- Print header
local header_parts = tbl.map(columns, function(col) return str.pad_right(col, widths[col]) end)
local separator_parts = tbl.map(columns, function(col) return str.repeat_("-", widths[col]) end)
local header_parts = tbl.map(columns, function(col) return string.pad_right(col, widths[col]) end)
local separator_parts = tbl.map(columns, function(col) return string.repeat_("-", widths[col]) end)
print(tbl.concat(header_parts, " | "))
print(tbl.concat(separator_parts, "-+-"))
@ -859,7 +858,7 @@ function mysql.print_results(results)
for _, row in ipairs(results) do
local value_parts = tbl.map(columns, function(col)
local value = tostring(row[col] or "")
return str.pad_right(value, widths[col])
return string.pad_right(value, widths[col])
end)
print(tbl.concat(value_parts, " | "))
end
@ -870,14 +869,14 @@ function mysql.escape_string(str_val)
if type(str_val) ~= "string" then
return tostring(str_val)
end
return str.replace(str_val, "'", "\\'")
return string.replace(str_val, "'", "\\'")
end
function mysql.escape_identifier(name)
if str.is_blank(name) then
if string.is_blank(name) then
error("Identifier name cannot be empty")
end
return str.template("`${name}`", {name = str.replace(name, "`", "``")})
return string.template("`${name}`", {name = string.replace(name, "`", "``")})
end
-- DSN builder helper
@ -888,10 +887,10 @@ function mysql.build_dsn(options)
local parts = {}
if options.username and not str.is_blank(options.username) then
if options.username and not string.is_blank(options.username) then
tbl.insert(parts, options.username)
if options.password and not str.is_blank(options.password) then
parts[#parts] = str.template("${user}:${pass}", {
if options.password and not string.is_blank(options.password) then
parts[#parts] = string.template("${user}:${pass}", {
user = parts[#parts],
pass = options.password
})
@ -899,22 +898,22 @@ function mysql.build_dsn(options)
parts[#parts] = parts[#parts] .. "@"
end
if options.protocol and not str.is_blank(options.protocol) then
tbl.insert(parts, str.template("${protocol}(", {protocol = options.protocol}))
if options.host and not str.is_blank(options.host) then
if options.protocol and not string.is_blank(options.protocol) then
tbl.insert(parts, string.template("${protocol}(", {protocol = options.protocol}))
if options.host and not string.is_blank(options.host) then
tbl.insert(parts, options.host)
if options.port then
parts[#parts] = str.template("${host}:${port}", {
parts[#parts] = string.template("${host}:${port}", {
host = parts[#parts],
port = tostring(options.port)
})
end
end
parts[#parts] = parts[#parts] .. ")"
elseif options.host and not str.is_blank(options.host) then
local host_part = str.template("tcp(${host}", {host = options.host})
elseif options.host and not string.is_blank(options.host) then
local host_part = string.template("tcp(${host}", {host = options.host})
if options.port then
host_part = str.template("${host}:${port}", {
host_part = string.template("${host}:${port}", {
host = host_part,
port = tostring(options.port)
})
@ -922,27 +921,27 @@ function mysql.build_dsn(options)
tbl.insert(parts, host_part .. ")")
end
if options.database and not str.is_blank(options.database) then
tbl.insert(parts, str.template("/${database}", {database = options.database}))
if options.database and not string.is_blank(options.database) then
tbl.insert(parts, string.template("/${database}", {database = options.database}))
end
-- Add parameters
local params = {}
if options.charset and not str.is_blank(options.charset) then
tbl.insert(params, str.template("charset=${charset}", {charset = options.charset}))
if options.charset and not string.is_blank(options.charset) then
tbl.insert(params, string.template("charset=${charset}", {charset = options.charset}))
end
if options.parseTime ~= nil then
tbl.insert(params, str.template("parseTime=${parse}", {parse = tostring(options.parseTime)}))
tbl.insert(params, string.template("parseTime=${parse}", {parse = tostring(options.parseTime)}))
end
if options.timeout and not str.is_blank(options.timeout) then
tbl.insert(params, str.template("timeout=${timeout}", {timeout = options.timeout}))
if options.timeout and not string.is_blank(options.timeout) then
tbl.insert(params, string.template("timeout=${timeout}", {timeout = options.timeout}))
end
if options.tls and not str.is_blank(options.tls) then
tbl.insert(params, str.template("tls=${tls}", {tls = options.tls}))
if options.tls and not string.is_blank(options.tls) then
tbl.insert(params, string.template("tls=${tls}", {tls = options.tls}))
end
if #params > 0 then
tbl.insert(parts, str.template("?${params}", {params = tbl.concat(params, "&")}))
tbl.insert(parts, string.template("?${params}", {params = tbl.concat(params, "&")}))
end
return tbl.concat(parts, "")

View File

@ -1,4 +1,3 @@
local str = require("string")
local tbl = require("table")
local postgres = {}
@ -25,7 +24,7 @@ function Connection:query(query_str, ...)
if not self._id then
error("Connection is closed")
end
query_str = str.normalize_whitespace(query_str)
query_str = string.normalize_whitespace(query_str)
return moonshark.sql_query(self._id, query_str, ...)
end
@ -33,7 +32,7 @@ function Connection:exec(query_str, ...)
if not self._id then
error("Connection is closed")
end
query_str = str.normalize_whitespace(query_str)
query_str = string.normalize_whitespace(query_str)
return moonshark.sql_exec(self._id, query_str, ...)
end
@ -85,20 +84,20 @@ function Connection:begin()
if not tx.active then
error("Transaction is not active")
end
if str.is_blank(name) then
if string.is_blank(name) then
error("Savepoint name cannot be empty")
end
return tx.conn:exec(str.template("SAVEPOINT ${name}", {name = name}))
return tx.conn:exec(string.template("SAVEPOINT ${name}", {name = name}))
end,
rollback_to = function(tx, name)
if not tx.active then
error("Transaction is not active")
end
if str.is_blank(name) then
if string.is_blank(name) then
error("Savepoint name cannot be empty")
end
return tx.conn:exec(str.template("ROLLBACK TO SAVEPOINT ${name}", {name = name}))
return tx.conn:exec(string.template("ROLLBACK TO SAVEPOINT ${name}", {name = name}))
end,
query = function(tx, query_str, ...)
@ -140,7 +139,7 @@ local function build_postgres_params(data)
local placeholders = {}
for i = 1, #keys do
tbl.insert(placeholders, str.template("$${num}", {num = tostring(i)}))
tbl.insert(placeholders, string.template("$${num}", {num = tostring(i)}))
end
return keys, values, placeholders, #keys
@ -148,20 +147,20 @@ end
-- Simplified query builders using table utilities
function Connection:insert(table_name, data, returning)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
local keys, values, placeholders = build_postgres_params(data)
local query = str.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", ")
})
if returning and not str.is_blank(returning) then
query = str.template("${query} RETURNING ${returning}", {
if returning and not string.is_blank(returning) then
query = string.template("${query} RETURNING ${returning}", {
query = query,
returning = returning
})
@ -172,25 +171,25 @@ function Connection:insert(table_name, data, returning)
end
function Connection:upsert(table_name, data, conflict_columns, returning)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
local keys, values, placeholders = build_postgres_params(data)
local updates = tbl.map(keys, function(key)
return str.template("${key} = EXCLUDED.${key}", {key = key})
return string.template("${key} = EXCLUDED.${key}", {key = key})
end)
local conflict_clause = ""
if conflict_columns then
if type(conflict_columns) == "string" then
conflict_clause = str.template("(${columns})", {columns = conflict_columns})
conflict_clause = string.template("(${columns})", {columns = conflict_columns})
else
conflict_clause = str.template("(${columns})", {columns = tbl.concat(conflict_columns, ", ")})
conflict_clause = string.template("(${columns})", {columns = tbl.concat(conflict_columns, ", ")})
end
end
local query = str.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT ${conflict} DO UPDATE SET ${updates}", {
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT ${conflict} DO UPDATE SET ${updates}", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", "),
@ -198,8 +197,8 @@ function Connection:upsert(table_name, data, conflict_columns, returning)
updates = tbl.concat(updates, ", ")
})
if returning and not str.is_blank(returning) then
query = str.template("${query} RETURNING ${returning}", {
if returning and not string.is_blank(returning) then
query = string.template("${query} RETURNING ${returning}", {
query = query,
returning = returning
})
@ -210,10 +209,10 @@ function Connection:upsert(table_name, data, conflict_columns, returning)
end
function Connection:update(table_name, data, where_clause, returning, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
if str.is_blank(where_clause) then
if string.is_blank(where_clause) then
error("WHERE clause cannot be empty for UPDATE")
end
@ -223,7 +222,7 @@ function Connection:update(table_name, data, where_clause, returning, ...)
local sets = {}
for i, key in ipairs(keys) do
tbl.insert(sets, str.template("${key} = $${num}", {
tbl.insert(sets, string.template("${key} = $${num}", {
key = key,
num = tostring(i)
}))
@ -235,18 +234,18 @@ function Connection:update(table_name, data, where_clause, returning, ...)
for i = 1, #where_args do
param_count = param_count + 1
tbl.insert(values, where_args[i])
where_clause_with_params = str.replace(where_clause_with_params, "?",
str.template("$${num}", {num = tostring(param_count)}), 1)
where_clause_with_params = string.replace(where_clause_with_params, "?",
string.template("$${num}", {num = tostring(param_count)}), 1)
end
local query = str.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
table = table_name,
sets = tbl.concat(sets, ", "),
where = where_clause_with_params
})
if returning and not str.is_blank(returning) then
query = str.template("${query} RETURNING ${returning}", {
if returning and not string.is_blank(returning) then
query = string.template("${query} RETURNING ${returning}", {
query = query,
returning = returning
})
@ -257,10 +256,10 @@ function Connection:update(table_name, data, where_clause, returning, ...)
end
function Connection:delete(table_name, where_clause, returning, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
if str.is_blank(where_clause) then
if string.is_blank(where_clause) then
error("WHERE clause cannot be empty for DELETE")
end
@ -270,17 +269,17 @@ function Connection:delete(table_name, where_clause, returning, ...)
local where_clause_with_params = where_clause
for i = 1, #where_args do
tbl.insert(values, where_args[i])
where_clause_with_params = str.replace(where_clause_with_params, "?",
str.template("$${num}", {num = tostring(i)}), 1)
where_clause_with_params = string.replace(where_clause_with_params, "?",
string.template("$${num}", {num = tostring(i)}), 1)
end
local query = str.template("DELETE FROM ${table} WHERE ${where}", {
local query = string.template("DELETE FROM ${table} WHERE ${where}", {
table = table_name,
where = where_clause_with_params
})
if returning and not str.is_blank(returning) then
query = str.template("${query} RETURNING ${returning}", {
if returning and not string.is_blank(returning) then
query = string.template("${query} RETURNING ${returning}", {
query = query,
returning = returning
})
@ -291,7 +290,7 @@ function Connection:delete(table_name, where_clause, returning, ...)
end
function Connection:select(table_name, columns, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -301,25 +300,25 @@ function Connection:select(table_name, columns, where_clause, ...)
end
local query
if where_clause and not str.is_blank(where_clause) then
if where_clause and not string.is_blank(where_clause) then
-- Handle WHERE clause parameters
local where_args = {...}
local values = {}
local where_clause_with_params = where_clause
for i = 1, #where_args do
tbl.insert(values, where_args[i])
where_clause_with_params = str.replace(where_clause_with_params, "?",
str.template("$${num}", {num = tostring(i)}), 1)
where_clause_with_params = string.replace(where_clause_with_params, "?",
string.template("$${num}", {num = tostring(i)}), 1)
end
query = str.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
columns = columns,
table = table_name,
where = where_clause_with_params
})
return self:query(query, unpack(values))
else
query = str.template("SELECT ${columns} FROM ${table}", {
query = string.template("SELECT ${columns} FROM ${table}", {
columns = columns,
table = table_name
})
@ -329,20 +328,20 @@ end
-- Enhanced PostgreSQL schema helpers
function Connection:table_exists(table_name, schema_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) 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",
str.trim(schema_name), str.trim(table_name)
string.trim(schema_name), string.trim(table_name)
)
return result ~= nil
end
function Connection:column_exists(table_name, column_name, schema_name)
if str.is_blank(table_name) or str.is_blank(column_name) then
if string.is_blank(table_name) or string.is_blank(column_name) then
return false
end
@ -350,29 +349,29 @@ function Connection:column_exists(table_name, column_name, schema_name)
local result = self:query_value([[
SELECT column_name FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
]], str.trim(schema_name), str.trim(table_name), str.trim(column_name))
]], string.trim(schema_name), string.trim(table_name), string.trim(column_name))
return result ~= nil
end
function Connection:create_table(table_name, schema)
if str.is_blank(table_name) or str.is_blank(schema) then
if string.is_blank(table_name) or string.is_blank(schema) then
error("Table name and schema cannot be empty")
end
local query = str.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", {
local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", {
table = table_name,
schema = str.trim(schema)
schema = string.trim(schema)
})
return self:exec(query)
end
function Connection:drop_table(table_name, cascade)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
local cascade_clause = cascade and " CASCADE" or ""
local query = str.template("DROP TABLE IF EXISTS ${table}${cascade}", {
local query = string.template("DROP TABLE IF EXISTS ${table}${cascade}", {
table = table_name,
cascade = cascade_clause
})
@ -380,24 +379,24 @@ function Connection:drop_table(table_name, cascade)
end
function Connection:add_column(table_name, column_def)
if str.is_blank(table_name) or str.is_blank(column_def) then
if string.is_blank(table_name) or string.is_blank(column_def) then
error("Table name and column definition cannot be empty")
end
local query = str.template("ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column}", {
local query = string.template("ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column}", {
table = table_name,
column = str.trim(column_def)
column = string.trim(column_def)
})
return self:exec(query)
end
function Connection:drop_column(table_name, column_name, cascade)
if str.is_blank(table_name) or str.is_blank(column_name) then
if string.is_blank(table_name) or string.is_blank(column_name) then
error("Table name and column name cannot be empty")
end
local cascade_clause = cascade and " CASCADE" or ""
local query = str.template("ALTER TABLE ${table} DROP COLUMN IF EXISTS ${column}${cascade}", {
local query = string.template("ALTER TABLE ${table} DROP COLUMN IF EXISTS ${column}${cascade}", {
table = table_name,
column = column_name,
cascade = cascade_clause
@ -406,15 +405,15 @@ function Connection:drop_column(table_name, column_name, cascade)
end
function Connection:create_index(index_name, table_name, columns, unique, method)
if str.is_blank(index_name) or str.is_blank(table_name) then
if string.is_blank(index_name) or string.is_blank(table_name) then
error("Index name and table name cannot be empty")
end
local unique_clause = unique and "UNIQUE " or ""
local method_clause = method and str.template(" USING ${method}", {method = str.upper(method)}) or ""
local method_clause = method and string.template(" USING ${method}", {method = string.upper(method)}) or ""
local columns_str = type(columns) == "table" and tbl.concat(columns, ", ") or tostring(columns)
local query = str.template("CREATE ${unique}INDEX IF NOT EXISTS ${index} ON ${table}${method} (${columns})", {
local query = string.template("CREATE ${unique}INDEX IF NOT EXISTS ${index} ON ${table}${method} (${columns})", {
unique = unique_clause,
index = index_name,
table = table_name,
@ -425,12 +424,12 @@ function Connection:create_index(index_name, table_name, columns, unique, method
end
function Connection:drop_index(index_name, cascade)
if str.is_blank(index_name) then
if string.is_blank(index_name) then
error("Index name cannot be empty")
end
local cascade_clause = cascade and " CASCADE" or ""
local query = str.template("DROP INDEX IF EXISTS ${index}${cascade}", {
local query = string.template("DROP INDEX IF EXISTS ${index}${cascade}", {
index = index_name,
cascade = cascade_clause
})
@ -440,32 +439,32 @@ end
-- PostgreSQL-specific functions
function Connection:vacuum(table_name, analyze)
local analyze_clause = analyze and " ANALYZE" or ""
local table_clause = table_name and str.template(" ${table}", {table = table_name}) or ""
return self:exec(str.template("VACUUM${analyze}${table}", {
local table_clause = table_name and string.template(" ${table}", {table = table_name}) or ""
return self:exec(string.template("VACUUM${analyze}${table}", {
analyze = analyze_clause,
table = table_clause
}))
end
function Connection:analyze(table_name)
local table_clause = table_name and str.template(" ${table}", {table = table_name}) or ""
return self:exec(str.template("ANALYZE${table}", {table = table_clause}))
local table_clause = table_name and string.template(" ${table}", {table = table_name}) or ""
return self:exec(string.template("ANALYZE${table}", {table = table_clause}))
end
function Connection:reindex(name, type)
if str.is_blank(name) then
if string.is_blank(name) then
error("Name cannot be empty for REINDEX")
end
type = type or "INDEX"
local valid_types = {"INDEX", "TABLE", "SCHEMA", "DATABASE", "SYSTEM"}
local type_upper = str.upper(type)
local type_upper = string.upper(type)
if not tbl.contains(valid_types, type_upper) then
error(str.template("Invalid REINDEX type: ${type}", {type = type}))
error(string.template("Invalid REINDEX type: ${type}", {type = type}))
end
return self:exec(str.template("REINDEX ${type} ${name}", {
return self:exec(string.template("REINDEX ${type} ${name}", {
type = type_upper,
name = name
}))
@ -473,17 +472,17 @@ end
-- PostgreSQL settings and introspection
function Connection:show(setting)
if str.is_blank(setting) then
if string.is_blank(setting) then
error("Setting name cannot be empty")
end
return self:query_value(str.template("SHOW ${setting}", {setting = setting}))
return self:query_value(string.template("SHOW ${setting}", {setting = setting}))
end
function Connection:set(setting, value)
if str.is_blank(setting) then
if string.is_blank(setting) then
error("Setting name cannot be empty")
end
return self:exec(str.template("SET ${setting} = ${value}", {
return self:exec(string.template("SET ${setting} = ${value}", {
setting = setting,
value = tostring(value)
}))
@ -508,11 +507,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",
str.trim(schema_name))
string.trim(schema_name))
end
function Connection:describe_table(table_name, schema_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -522,64 +521,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
]], str.trim(schema_name), str.trim(table_name))
]], string.trim(schema_name), string.trim(table_name))
end
-- JSON/JSONB helpers
function Connection:json_extract(column, path)
if str.is_blank(column) or str.is_blank(path) then
if string.is_blank(column) or string.is_blank(path) then
error("Column and path cannot be empty")
end
return str.template("${column}->'${path}'", {column = column, path = path})
return string.template("${column}->'${path}'", {column = column, path = path})
end
function Connection:json_extract_text(column, path)
if str.is_blank(column) or str.is_blank(path) then
if string.is_blank(column) or string.is_blank(path) then
error("Column and path cannot be empty")
end
return str.template("${column}->>'${path}'", {column = column, path = path})
return string.template("${column}->>'${path}'", {column = column, path = path})
end
function Connection:jsonb_contains(column, value)
if str.is_blank(column) or str.is_blank(value) then
if string.is_blank(column) or string.is_blank(value) then
error("Column and value cannot be empty")
end
return str.template("${column} @> '${value}'", {column = column, value = value})
return string.template("${column} @> '${value}'", {column = column, value = value})
end
function Connection:jsonb_contained_by(column, value)
if str.is_blank(column) or str.is_blank(value) then
if string.is_blank(column) or string.is_blank(value) then
error("Column and value cannot be empty")
end
return str.template("${column} <@ '${value}'", {column = column, value = value})
return string.template("${column} <@ '${value}'", {column = column, value = value})
end
-- Array helpers
function Connection:array_contains(column, value)
if str.is_blank(column) then
if string.is_blank(column) then
error("Column cannot be empty")
end
return str.template("$1 = ANY(${column})", {column = column})
return string.template("$1 = ANY(${column})", {column = column})
end
function Connection:array_length(column)
if str.is_blank(column) then
if string.is_blank(column) then
error("Column cannot be empty")
end
return str.template("array_length(${column}, 1)", {column = column})
return string.template("array_length(${column}, 1)", {column = column})
end
-- Connection management
function postgres.parse_dsn(dsn)
if str.is_blank(dsn) then
if string.is_blank(dsn) then
return nil, "DSN cannot be empty"
end
local parts = {}
for pair in str.trim(dsn):gmatch("[^%s]+") do
for pair in string.trim(dsn):gmatch("[^%s]+") do
local key, value = pair:match("([^=]+)=(.+)")
if key and value then
parts[str.trim(key)] = str.trim(value)
parts[string.trim(key)] = string.trim(value)
end
end
@ -587,11 +586,11 @@ function postgres.parse_dsn(dsn)
end
function postgres.connect(dsn)
if str.is_blank(dsn) then
if string.is_blank(dsn) then
error("DSN cannot be empty")
end
local conn_id = moonshark.sql_connect("postgres", str.trim(dsn))
local conn_id = moonshark.sql_connect("postgres", string.trim(dsn))
if conn_id then
local conn = {_id = conn_id}
setmetatable(conn, Connection)
@ -664,14 +663,14 @@ function postgres.migrate(dsn, migrations, schema)
local error_msg = ""
for _, migration in ipairs(migrations) do
if not migration.name or str.is_blank(migration.name) then
if not migration.name or string.is_blank(migration.name) then
error_msg = "Migration must have a non-empty name"
success = false
break
end
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = $1",
str.trim(migration.name))
string.trim(migration.name))
if not existing then
local ok, err = pcall(function()
if type(migration.up) == "string" then
@ -684,11 +683,11 @@ function postgres.migrate(dsn, migrations, schema)
end)
if ok then
conn:exec("INSERT INTO _migrations (name) VALUES ($1)", str.trim(migration.name))
print(str.template("Applied migration: ${name}", {name = migration.name}))
conn:exec("INSERT INTO _migrations (name) VALUES ($1)", string.trim(migration.name))
print(string.template("Applied migration: ${name}", {name = migration.name}))
else
success = false
error_msg = str.template("Migration '${name}' failed: ${error}", {
error_msg = string.template("Migration '${name}' failed: ${error}", {
name = migration.name,
error = err or "unknown error"
})
@ -715,7 +714,7 @@ function postgres.to_array(results, column_name)
return {}
end
if str.is_blank(column_name) then
if string.is_blank(column_name) then
error("Column name cannot be empty")
end
@ -727,7 +726,7 @@ function postgres.to_map(results, key_column, value_column)
return {}
end
if str.is_blank(key_column) then
if string.is_blank(key_column) then
error("Key column name cannot be empty")
end
@ -744,7 +743,7 @@ function postgres.group_by(results, column_name)
return {}
end
if str.is_blank(column_name) then
if string.is_blank(column_name) then
error("Column name cannot be empty")
end
@ -764,19 +763,19 @@ function postgres.print_results(results)
-- Calculate column widths
local widths = {}
for _, col in ipairs(columns) do
widths[col] = str.length(col)
widths[col] = string.length(col)
end
for _, row in ipairs(results) do
for _, col in ipairs(columns) do
local value = tostring(row[col] or "")
widths[col] = math.max(widths[col], str.length(value))
widths[col] = math.max(widths[col], string.length(value))
end
end
-- Print header
local header_parts = tbl.map(columns, function(col) return str.pad_right(col, widths[col]) end)
local separator_parts = tbl.map(columns, function(col) return str.repeat_("-", widths[col]) end)
local header_parts = tbl.map(columns, function(col) return string.pad_right(col, widths[col]) end)
local separator_parts = tbl.map(columns, function(col) return string.repeat_("-", widths[col]) end)
print(tbl.concat(header_parts, " | "))
print(tbl.concat(separator_parts, "-+-"))
@ -785,22 +784,22 @@ function postgres.print_results(results)
for _, row in ipairs(results) do
local value_parts = tbl.map(columns, function(col)
local value = tostring(row[col] or "")
return str.pad_right(value, widths[col])
return string.pad_right(value, widths[col])
end)
print(tbl.concat(value_parts, " | "))
end
end
function postgres.escape_identifier(name)
if str.is_blank(name) then
if string.is_blank(name) then
error("Identifier name cannot be empty")
end
return str.template('"${name}"', {name = str.replace(name, '"', '""')})
return string.template('"${name}"', {name = string.replace(name, '"', '""')})
end
function postgres.escape_literal(value)
if type(value) == "string" then
return str.template("'${value}'", {value = str.replace(value, "'", "''")})
return string.template("'${value}'", {value = string.replace(value, "'", "''")})
end
return tostring(value)
end

View File

@ -1,4 +1,3 @@
local str = require("string")
local tbl = require("table")
local sqlite = {}
@ -25,7 +24,7 @@ function Connection:query(query_str, ...)
if not self._id then
error("Connection is closed")
end
query_str = str.normalize_whitespace(query_str)
query_str = string.normalize_whitespace(query_str)
return moonshark.sql_query(self._id, query_str, ...)
end
@ -33,7 +32,7 @@ function Connection:exec(query_str, ...)
if not self._id then
error("Connection is closed")
end
query_str = str.normalize_whitespace(query_str)
query_str = string.normalize_whitespace(query_str)
return moonshark.sql_exec(self._id, query_str, ...)
end
@ -115,7 +114,7 @@ end
-- Simplified query builders using table utilities
function Connection:insert(table_name, data)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -123,7 +122,7 @@ function Connection:insert(table_name, data)
local values = tbl.values(data)
local placeholders = tbl.map(keys, function() return "?" end)
local query = str.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders})", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", ")
@ -133,7 +132,7 @@ function Connection:insert(table_name, data)
end
function Connection:upsert(table_name, data, conflict_columns)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -141,19 +140,19 @@ function Connection:upsert(table_name, data, conflict_columns)
local values = tbl.values(data)
local placeholders = tbl.map(keys, function() return "?" end)
local updates = tbl.map(keys, function(key)
return str.template("${key} = excluded.${key}", {key = key})
return string.template("${key} = excluded.${key}", {key = key})
end)
local conflict_clause = ""
if conflict_columns then
if type(conflict_columns) == "string" then
conflict_clause = str.template("(${columns})", {columns = conflict_columns})
conflict_clause = string.template("(${columns})", {columns = conflict_columns})
else
conflict_clause = str.template("(${columns})", {columns = tbl.concat(conflict_columns, ", ")})
conflict_clause = string.template("(${columns})", {columns = tbl.concat(conflict_columns, ", ")})
end
end
local query = str.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT ${conflict} DO UPDATE SET ${updates}", {
local query = string.template("INSERT INTO ${table} (${columns}) VALUES (${placeholders}) ON CONFLICT ${conflict} DO UPDATE SET ${updates}", {
table = table_name,
columns = tbl.concat(keys, ", "),
placeholders = tbl.concat(placeholders, ", "),
@ -165,20 +164,20 @@ function Connection:upsert(table_name, data, conflict_columns)
end
function Connection:update(table_name, data, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
if str.is_blank(where_clause) then
if string.is_blank(where_clause) then
error("WHERE clause cannot be empty for UPDATE")
end
local keys = tbl.keys(data)
local values = tbl.values(data)
local sets = tbl.map(keys, function(key)
return str.template("${key} = ?", {key = key})
return string.template("${key} = ?", {key = key})
end)
local query = str.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
local query = string.template("UPDATE ${table} SET ${sets} WHERE ${where}", {
table = table_name,
sets = tbl.concat(sets, ", "),
where = where_clause
@ -192,14 +191,14 @@ function Connection:update(table_name, data, where_clause, ...)
end
function Connection:delete(table_name, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
if str.is_blank(where_clause) then
if string.is_blank(where_clause) then
error("WHERE clause cannot be empty for DELETE")
end
local query = str.template("DELETE FROM ${table} WHERE ${where}", {
local query = string.template("DELETE FROM ${table} WHERE ${where}", {
table = table_name,
where = where_clause
})
@ -207,7 +206,7 @@ function Connection:delete(table_name, where_clause, ...)
end
function Connection:select(table_name, columns, where_clause, ...)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
@ -217,15 +216,15 @@ function Connection:select(table_name, columns, where_clause, ...)
end
local query
if where_clause and not str.is_blank(where_clause) then
query = str.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
if where_clause and not string.is_blank(where_clause) then
query = string.template("SELECT ${columns} FROM ${table} WHERE ${where}", {
columns = columns,
table = table_name,
where = where_clause
})
return self:query(query, ...)
else
query = str.template("SELECT ${columns} FROM ${table}", {
query = string.template("SELECT ${columns} FROM ${table}", {
columns = columns,
table = table_name
})
@ -235,73 +234,73 @@ end
-- Schema helpers
function Connection:table_exists(table_name)
if str.is_blank(table_name) then
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=?",
str.trim(table_name)
string.trim(table_name)
)
return result ~= nil
end
function Connection:column_exists(table_name, column_name)
if str.is_blank(table_name) or str.is_blank(column_name) then
if string.is_blank(table_name) or string.is_blank(column_name) then
return false
end
local result = self:query(str.template("PRAGMA table_info(${table})", {table = table_name}))
local result = self:query(string.template("PRAGMA table_info(${table})", {table = table_name}))
if result then
return tbl.any(result, function(row)
return str.iequals(row.name, str.trim(column_name))
return string.iequals(row.name, string.trim(column_name))
end)
end
return false
end
function Connection:create_table(table_name, schema)
if str.is_blank(table_name) or str.is_blank(schema) then
if string.is_blank(table_name) or string.is_blank(schema) then
error("Table name and schema cannot be empty")
end
local query = str.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", {
local query = string.template("CREATE TABLE IF NOT EXISTS ${table} (${schema})", {
table = table_name,
schema = str.trim(schema)
schema = string.trim(schema)
})
return self:exec(query)
end
function Connection:drop_table(table_name)
if str.is_blank(table_name) then
if string.is_blank(table_name) then
error("Table name cannot be empty")
end
local query = str.template("DROP TABLE IF EXISTS ${table}", {table = table_name})
local query = string.template("DROP TABLE IF EXISTS ${table}", {table = table_name})
return self:exec(query)
end
function Connection:add_column(table_name, column_def)
if str.is_blank(table_name) or str.is_blank(column_def) then
if string.is_blank(table_name) or string.is_blank(column_def) then
error("Table name and column definition cannot be empty")
end
local query = str.template("ALTER TABLE ${table} ADD COLUMN ${column}", {
local query = string.template("ALTER TABLE ${table} ADD COLUMN ${column}", {
table = table_name,
column = str.trim(column_def)
column = string.trim(column_def)
})
return self:exec(query)
end
function Connection:create_index(index_name, table_name, columns, unique)
if str.is_blank(index_name) or str.is_blank(table_name) then
if string.is_blank(index_name) or string.is_blank(table_name) 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 tbl.concat(columns, ", ") or tostring(columns)
local query = str.template("CREATE ${unique}INDEX IF NOT EXISTS ${index} ON ${table} (${columns})", {
local query = string.template("CREATE ${unique}INDEX IF NOT EXISTS ${index} ON ${table} (${columns})", {
unique = unique_clause,
index = index_name,
table = table_name,
@ -311,11 +310,11 @@ function Connection:create_index(index_name, table_name, columns, unique)
end
function Connection:drop_index(index_name)
if str.is_blank(index_name) then
if string.is_blank(index_name) then
error("Index name cannot be empty")
end
local query = str.template("DROP INDEX IF EXISTS ${index}", {index = index_name})
local query = string.template("DROP INDEX IF EXISTS ${index}", {index = index_name})
return self:exec(query)
end
@ -334,29 +333,29 @@ end
function Connection:foreign_keys(enabled)
local value = enabled and "ON" or "OFF"
return self:exec(str.template("PRAGMA foreign_keys = ${value}", {value = value}))
return self:exec(string.template("PRAGMA foreign_keys = ${value}", {value = value}))
end
function Connection:journal_mode(mode)
mode = mode or "WAL"
local valid_modes = {"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}
if not tbl.contains(tbl.map(valid_modes, str.upper), str.upper(mode)) then
if not tbl.contains(tbl.map(valid_modes, string.upper), string.upper(mode)) then
error("Invalid journal mode: " .. mode)
end
return self:query(str.template("PRAGMA journal_mode = ${mode}", {mode = str.upper(mode)}))
return self:query(string.template("PRAGMA journal_mode = ${mode}", {mode = string.upper(mode)}))
end
function Connection:synchronous(level)
level = level or "NORMAL"
local valid_levels = {"OFF", "NORMAL", "FULL", "EXTRA"}
if not tbl.contains(valid_levels, str.upper(level)) then
if not tbl.contains(valid_levels, string.upper(level)) then
error("Invalid synchronous level: " .. level)
end
return self:exec(str.template("PRAGMA synchronous = ${level}", {level = str.upper(level)}))
return self:exec(string.template("PRAGMA synchronous = ${level}", {level = string.upper(level)}))
end
function Connection:cache_size(size)
@ -364,18 +363,18 @@ function Connection:cache_size(size)
if type(size) ~= "number" then
error("Cache size must be a number")
end
return self:exec(str.template("PRAGMA cache_size = ${size}", {size = tostring(size)}))
return self:exec(string.template("PRAGMA cache_size = ${size}", {size = tostring(size)}))
end
function Connection:temp_store(mode)
mode = mode or "MEMORY"
local valid_modes = {"DEFAULT", "FILE", "MEMORY"}
if not tbl.contains(valid_modes, str.upper(mode)) then
if not tbl.contains(valid_modes, string.upper(mode)) then
error("Invalid temp_store mode: " .. mode)
end
return self:exec(str.template("PRAGMA temp_store = ${mode}", {mode = str.upper(mode)}))
return self:exec(string.template("PRAGMA temp_store = ${mode}", {mode = string.upper(mode)}))
end
-- Connection management
@ -383,8 +382,8 @@ function sqlite.open(database_path)
database_path = database_path or ":memory:"
if database_path ~= ":memory:" then
database_path = str.trim(database_path)
if str.is_blank(database_path) then
database_path = string.trim(database_path)
if string.is_blank(database_path) then
database_path = ":memory:"
end
end
@ -404,7 +403,7 @@ sqlite.connect = sqlite.open
function sqlite.query(database_path, query_str, ...)
local conn = sqlite.open(database_path)
if not conn then
error(str.template("Failed to open SQLite database: ${path}", {
error(string.template("Failed to open SQLite database: ${path}", {
path = database_path or ":memory:"
}))
end
@ -417,7 +416,7 @@ end
function sqlite.exec(database_path, query_str, ...)
local conn = sqlite.open(database_path)
if not conn then
error(str.template("Failed to open SQLite database: ${path}", {
error(string.template("Failed to open SQLite database: ${path}", {
path = database_path or ":memory:"
}))
end
@ -465,14 +464,14 @@ function sqlite.migrate(database_path, migrations)
local error_msg = ""
for _, migration in ipairs(migrations) do
if not migration.name or str.is_blank(migration.name) then
if not migration.name or string.is_blank(migration.name) then
error_msg = "Migration must have a non-empty name"
success = false
break
end
local existing = conn:query_value("SELECT id FROM _migrations WHERE name = ?",
str.trim(migration.name))
string.trim(migration.name))
if not existing then
local ok, err = pcall(function()
if type(migration.up) == "string" then
@ -485,11 +484,11 @@ function sqlite.migrate(database_path, migrations)
end)
if ok then
conn:exec("INSERT INTO _migrations (name) VALUES (?)", str.trim(migration.name))
print(str.template("Applied migration: ${name}", {name = migration.name}))
conn:exec("INSERT INTO _migrations (name) VALUES (?)", string.trim(migration.name))
print(string.template("Applied migration: ${name}", {name = migration.name}))
else
success = false
error_msg = str.template("Migration '${name}' failed: ${error}", {
error_msg = string.template("Migration '${name}' failed: ${error}", {
name = migration.name,
error = err or "unknown error"
})
@ -516,7 +515,7 @@ function sqlite.to_array(results, column_name)
return {}
end
if str.is_blank(column_name) then
if string.is_blank(column_name) then
error("Column name cannot be empty")
end
@ -528,7 +527,7 @@ function sqlite.to_map(results, key_column, value_column)
return {}
end
if str.is_blank(key_column) then
if string.is_blank(key_column) then
error("Key column name cannot be empty")
end
@ -545,7 +544,7 @@ function sqlite.group_by(results, column_name)
return {}
end
if str.is_blank(column_name) then
if string.is_blank(column_name) then
error("Column name cannot be empty")
end
@ -563,18 +562,18 @@ function sqlite.print_results(results)
tbl.sort(columns)
-- Calculate column widths
local widths = tbl.map_values(tbl.to_map(columns, function(col) return col end, function(col) return str.length(col) end), function(width) return width end)
local widths = tbl.map_values(tbl.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 value = tostring(row[col] or "")
widths[col] = math.max(widths[col], str.length(value))
widths[col] = math.max(widths[col], string.length(value))
end
end
-- Print header
local header_parts = tbl.map(columns, function(col) return str.pad_right(col, widths[col]) end)
local separator_parts = tbl.map(columns, function(col) return str.repeat_("-", widths[col]) end)
local header_parts = tbl.map(columns, function(col) return string.pad_right(col, widths[col]) end)
local separator_parts = tbl.map(columns, function(col) return string.repeat_("-", widths[col]) end)
print(tbl.concat(header_parts, " | "))
print(tbl.concat(separator_parts, "-+-"))
@ -583,7 +582,7 @@ function sqlite.print_results(results)
for _, row in ipairs(results) do
local value_parts = tbl.map(columns, function(col)
local value = tostring(row[col] or "")
return str.pad_right(value, widths[col])
return string.pad_right(value, widths[col])
end)
print(tbl.concat(value_parts, " | "))
end