Adds LumeniteDB module for SQLite database access
Introduces a new LumeniteDB module that provides a Lua API for interacting with SQLite databases. This includes: - Opening/creating databases - Defining models and columns with primary key and default value support - Creating tables - Adding and committing session data for inserting/updating rows - Selecting data from tables - Support for transactions, last insert id, and delete operations - Query building API with filter, order, limit and count capabilities - Logging SQL queries for debugging.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// src/modules/LumeniteDb.cpp
|
||||
// Hours spent: 37
|
||||
// Gigabite: im sad right now, and my eyes hurt. but it works now. and thats what matters.
|
||||
|
||||
#include "LumeniteDB.h"
|
||||
#include <sqlite3.h>
|
||||
@@ -9,18 +10,19 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <ctime>
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
|
||||
#include "../ErrorHandler.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string LumeniteDB::db_filename{};
|
||||
bool LumeniteDB::sql_log_enabled{};
|
||||
std::ofstream LumeniteDB::sql_log_stream{};
|
||||
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
std::map<std::string, LumeniteDB::Model> LumeniteDB::models;
|
||||
LumeniteDB::Session LumeniteDB::session;
|
||||
LumeniteDB::DB *LumeniteDB::db_instance = nullptr;
|
||||
@@ -29,6 +31,7 @@ static int instance_newindex(lua_State *L);
|
||||
|
||||
static void bind_filter_args(lua_State *L, sqlite3_stmt *stmt);
|
||||
|
||||
static constexpr const char *LM_HIDDEN_TABLE_KEY = "__lm_table__";
|
||||
|
||||
static std::string current_timestamp()
|
||||
{
|
||||
@@ -39,65 +42,47 @@ static std::string current_timestamp()
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
// ── Exec runner (no return) ───────────────────────────────────────────────
|
||||
static void run_sql_exec(lua_State *L, const std::string &sql)
|
||||
static void log_sql(const std::string &sql)
|
||||
{
|
||||
if (LumeniteDB::sql_log_stream.is_open()) {
|
||||
LumeniteDB::sql_log_stream
|
||||
<< "[" << current_timestamp() << "] " << sql << "\n";
|
||||
LumeniteDB::sql_log_stream << "[" << current_timestamp() << "] " << sql << "\n";
|
||||
LumeniteDB::sql_log_stream.flush();
|
||||
}
|
||||
if (!LumeniteDB::db_instance)
|
||||
luaL_error(L, "No DB connection. Call db.open() first");
|
||||
if (!LumeniteDB::db_instance->exec(sql))
|
||||
luaL_error(L, "SQL error: %s",
|
||||
LumeniteDB::db_instance->lastError().c_str());
|
||||
}
|
||||
|
||||
// ── Query runner (returns Lua table + prints rows) ─────────────────────────
|
||||
static int run_sql_query(lua_State *L, const std::string &sql)
|
||||
static void require_db(lua_State *L)
|
||||
{
|
||||
// 1) log the SQL
|
||||
if (LumeniteDB::sql_log_stream.is_open()) {
|
||||
LumeniteDB::sql_log_stream
|
||||
<< "[" << current_timestamp() << "] " << sql << "\n";
|
||||
LumeniteDB::sql_log_stream.flush();
|
||||
}
|
||||
if (!LumeniteDB::db_instance)
|
||||
luaL_error(L, "No DB connection. Call db.open() first");
|
||||
}
|
||||
|
||||
static void run_sql_exec(lua_State *L, const std::string &sql)
|
||||
{
|
||||
log_sql(sql);
|
||||
require_db(L);
|
||||
if (!LumeniteDB::db_instance->exec(sql)) {
|
||||
luaL_error(L, "SQL error: %s", LumeniteDB::db_instance->lastError().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static int run_sql_query(lua_State *L, const std::string &sql)
|
||||
{
|
||||
log_sql(sql);
|
||||
require_db(L);
|
||||
|
||||
// 2) prepare
|
||||
sqlite3_stmt *stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(
|
||||
LumeniteDB::db_instance->handle,
|
||||
sql.c_str(), -1, &stmt, nullptr
|
||||
) != SQLITE_OK) {
|
||||
luaL_error(L, "SQLite prepare failed: %s",
|
||||
sqlite3_errmsg(LumeniteDB::db_instance->handle));
|
||||
if (sqlite3_prepare_v2(LumeniteDB::db_instance->handle, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
luaL_error(L, "SQLite prepare failed: %s", sqlite3_errmsg(LumeniteDB::db_instance->handle));
|
||||
}
|
||||
|
||||
// 3) bind any filter args
|
||||
// bind any filter args from caller table[__filter_args]
|
||||
bind_filter_args(L, stmt);
|
||||
|
||||
// 4) fetch & print rows
|
||||
lua_newtable(L);
|
||||
int row = 1;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int cols = sqlite3_column_count(stmt);
|
||||
|
||||
//// —— debug print
|
||||
//std::cout << "[lumenite.db.result] row " << (row - 1) << ": ";
|
||||
//for (int c = 0; c < cols; ++c) {
|
||||
// const char *col = sqlite3_column_name(stmt, c);
|
||||
// const unsigned char *txt = sqlite3_column_text(stmt, c);
|
||||
// const char *val = txt ? reinterpret_cast<const char *>(txt) : "NULL";
|
||||
// std::cout << col << "=" << val;
|
||||
// if (c + 1 < cols) std::cout << ", ";
|
||||
//}
|
||||
//std::cout << "\n";
|
||||
|
||||
// —— build Lua table
|
||||
lua_newtable(L);
|
||||
for (int c = 0; c < cols; ++c) {
|
||||
const char *col = sqlite3_column_name(stmt, c);
|
||||
@@ -109,36 +94,49 @@ static int run_sql_query(lua_State *L, const std::string &sql)
|
||||
lua_rawseti(L, -2, row++);
|
||||
}
|
||||
|
||||
// 5) cleanup & return the table
|
||||
sqlite3_finalize(stmt);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static void bind_filter_args(lua_State *L, sqlite3_stmt *stmt)
|
||||
{
|
||||
// Caller’s query table is expected at stack index 1
|
||||
lua_getfield(L, 1, "__filter_args");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
int n = (int) lua_rawlen(L, -1);
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
lua_rawgeti(L, -1, i);
|
||||
if (lua_isinteger(L, -1)) sqlite3_bind_int(stmt, i, (int) lua_tointeger(L, -1));
|
||||
else if (lua_isnumber(L, -1)) sqlite3_bind_double(stmt, i, lua_tonumber(L, -1));
|
||||
else if (lua_isstring(L, -1)) sqlite3_bind_text(stmt, i, lua_tostring(L, -1), -1, SQLITE_TRANSIENT);
|
||||
else sqlite3_bind_null(stmt, i);
|
||||
lua_pop(L, 1);
|
||||
if (lua_isinteger(L, -1)) {
|
||||
sqlite3_bind_int64(stmt, i, (sqlite3_int64) lua_tointeger(L, -1));
|
||||
} else if (lua_isnumber(L, -1)) {
|
||||
sqlite3_bind_double(stmt, i, lua_tonumber(L, -1));
|
||||
} else if (lua_isstring(L, -1)) {
|
||||
sqlite3_bind_text(stmt, i, lua_tostring(L, -1), -1, SQLITE_TRANSIENT);
|
||||
} else if (lua_isnil(L, -1)) {
|
||||
sqlite3_bind_null(stmt, i);
|
||||
} else {
|
||||
// Fallback: convert to string
|
||||
lua_getglobal(L, "tostring");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 1);
|
||||
const char *s = lua_tostring(L, -1);
|
||||
sqlite3_bind_text(stmt, i, s ? s : "", -1, SQLITE_TRANSIENT);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1); // pop value
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
lua_pop(L, 1); // pop __filter_args table
|
||||
}
|
||||
|
||||
static int proxy_index(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, 1, "__data"); // push data table
|
||||
lua_pushvalue(L, 2); // push key
|
||||
lua_gettable(L, -2); // data[key]
|
||||
lua_getfield(L, 1, "__data");
|
||||
lua_pushvalue(L, 2);
|
||||
lua_gettable(L, -2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -147,22 +145,22 @@ static int proxy_newindex(lua_State *L)
|
||||
// upvalue #1 = tablename
|
||||
const char *tablename = lua_tostring(L, lua_upvalueindex(1));
|
||||
|
||||
// 1) rawset into proxy.__data
|
||||
lua_getfield(L, 1, "__data"); // stack: proxy, key, val, data
|
||||
// rawset into proxy.__data
|
||||
lua_getfield(L, 1, "__data"); // [proxy, key, val, data]
|
||||
lua_pushvalue(L, 2); // key
|
||||
lua_pushvalue(L, 3); // val
|
||||
lua_rawset(L, -3); // data[key] = val
|
||||
lua_pop(L, 1); // pop data
|
||||
|
||||
// 2) queue the UPDATE
|
||||
// queue UPDATE using id
|
||||
lua_getfield(L, 1, "__data");
|
||||
lua_getfield(L, -1, "id");
|
||||
const char *id = lua_tostring(L, -1);
|
||||
lua_pop(L, 2);
|
||||
|
||||
LumeniteDB::Update upd;
|
||||
upd.tablename = tablename;
|
||||
upd.changes["id"] = id;
|
||||
upd.tablename = tablename ? tablename : "";
|
||||
upd.changes["id"] = id ? id : "";
|
||||
lua_pushvalue(L, 3);
|
||||
const char *v = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
@@ -172,7 +170,6 @@ static int proxy_newindex(lua_State *L)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static void clone_query(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
@@ -180,24 +177,17 @@ static void clone_query(lua_State *L)
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, 1) != 0) {
|
||||
// stack: [ ... , key, value ]
|
||||
lua_pushvalue(L, -2); // copy key
|
||||
lua_pushvalue(L, -2); // copy value
|
||||
lua_settable(L, newTbl); // newTbl[key] = value
|
||||
lua_pop(L, 1); // pop value, keep key for next()
|
||||
lua_pushvalue(L, -2); // key
|
||||
lua_pushvalue(L, -2); // value
|
||||
lua_settable(L, newTbl);
|
||||
lua_pop(L, 1); // pop value, keep key
|
||||
}
|
||||
|
||||
// Replace slot‑1 with our newTbl
|
||||
lua_replace(L, 1);
|
||||
lua_replace(L, 1); // replace arg-1 with the clone
|
||||
}
|
||||
|
||||
|
||||
static void register_default_query_methods(lua_State *L,
|
||||
int idx,
|
||||
const std::string &tablename)
|
||||
static void register_default_query_methods(lua_State *L, int idx, const std::string &tablename)
|
||||
{
|
||||
// ─── order_by(expr) ──────────────────────────────────────────────────────
|
||||
// Just remember the ORDER BY clause; don't execute yet.
|
||||
// order_by(expr)
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
@@ -210,8 +200,7 @@ static void register_default_query_methods(lua_State *L,
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "order_by");
|
||||
|
||||
// ─── limit(n) ────────────────────────────────────────────────────────────
|
||||
// Just remember the LIMIT; don't execute yet.
|
||||
// limit(n)
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
@@ -224,8 +213,7 @@ static void register_default_query_methods(lua_State *L,
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "limit");
|
||||
|
||||
// ─── filter_by({ k=v, … }) ────────────────────────────────────────────────
|
||||
// Build a WHERE‑fragment and args list; don't execute until first()/all().
|
||||
// filter_by({k=v,...})
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
@@ -244,18 +232,14 @@ static void register_default_query_methods(lua_State *L,
|
||||
if (first) {
|
||||
where << "WHERE ";
|
||||
first = false;
|
||||
} else {
|
||||
where << " AND ";
|
||||
}
|
||||
} else { where << " AND "; }
|
||||
where << col << " = ?";
|
||||
|
||||
// collect the value
|
||||
lua_pushvalue(L, -1);
|
||||
lua_rawseti(L, argsIdx, lua_rawlen(L, argsIdx) + 1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// store just the fragment (no SELECT, no semicolon)
|
||||
lua_pushstring(L, where.str().c_str());
|
||||
lua_setfield(L, 1, "__filter_sql");
|
||||
lua_pushvalue(L, argsIdx);
|
||||
@@ -266,40 +250,40 @@ static void register_default_query_methods(lua_State *L,
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "filter_by");
|
||||
|
||||
// … inside register_default_query_methods …
|
||||
|
||||
// ─── get(id) ─────────────────────────────────────────────────────────────
|
||||
// get(id) -> proxy or nil
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
const char *tbl = lua_tostring(L, lua_upvalueindex(1));
|
||||
// pick arg 1 or 2 for colon vs dot call
|
||||
int arg = (lua_istable(L, 1) && (lua_isinteger(L, 2) || lua_isstring(L, 2))) ? 2 : 1;
|
||||
if (!lua_isinteger(L, arg) && !lua_isstring(L, arg))
|
||||
return luaL_error(L, "Expected integer or string ID");
|
||||
|
||||
// stash single filter arg
|
||||
// Clone the query table (slot 1) and attach __filter_args there
|
||||
clone_query(L);
|
||||
lua_newtable(L);
|
||||
lua_pushvalue(L, arg);
|
||||
lua_rawseti(L, -2, 1);
|
||||
lua_setfield(L, 1, "__filter_args");
|
||||
|
||||
std::string sql = std::string("SELECT * FROM ") + tbl +
|
||||
" WHERE id = ? LIMIT 1;";
|
||||
// pushes a 1‑element array of rows
|
||||
run_sql_query(L, sql);
|
||||
std::string sql = std::string("SELECT * FROM ") + tbl + " WHERE id = ? LIMIT 1;";
|
||||
run_sql_query(L, sql); // pushes result-table
|
||||
|
||||
// extract the first row
|
||||
lua_rawgeti(L, -1, 1);
|
||||
lua_replace(L, -2); // stack now: [ row_table ]
|
||||
bool hasRow = !lua_isnil(L, -1);
|
||||
if (!hasRow) {
|
||||
lua_pop(L, 2); // pop nil row + result-table
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// wrap in proxy
|
||||
lua_newtable(L); // [ row, proxy ]
|
||||
lua_pushvalue(L, -2); // [ row, proxy, row ]
|
||||
lua_setfield(L, -2, "__data"); // proxy.__data = row
|
||||
lua_replace(L, -2); // stack: [ row_table ]
|
||||
|
||||
// build metatable
|
||||
lua_newtable(L); // [ row, proxy, mt ]
|
||||
lua_newtable(L); // proxy
|
||||
lua_pushvalue(L, -2); // row
|
||||
lua_setfield(L, -2, "__data");
|
||||
|
||||
lua_newtable(L); // mt
|
||||
lua_pushcfunction(L, proxy_index);
|
||||
lua_setfield(L, -2, "__index");
|
||||
lua_pushstring(L, tbl);
|
||||
@@ -307,12 +291,12 @@ static void register_default_query_methods(lua_State *L,
|
||||
lua_setfield(L, -2, "__newindex");
|
||||
lua_setmetatable(L, -2); // set mt on proxy
|
||||
|
||||
lua_remove(L, -2); // remove raw row, leave [ proxy ]
|
||||
lua_remove(L, -2); // remove raw row
|
||||
return 1;
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "get");
|
||||
|
||||
// ─── first() ────────────────────────────────────────────────────────────
|
||||
// first() -> proxy or nil
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
@@ -332,6 +316,13 @@ static void register_default_query_methods(lua_State *L,
|
||||
run_sql_query(L, ss.str());
|
||||
|
||||
lua_rawgeti(L, -1, 1);
|
||||
bool hasRow = !lua_isnil(L, -1);
|
||||
if (!hasRow) {
|
||||
lua_pop(L, 2); // nil + result table
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
lua_replace(L, -2); // [ row_table ]
|
||||
|
||||
lua_newtable(L);
|
||||
@@ -351,8 +342,7 @@ static void register_default_query_methods(lua_State *L,
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "first");
|
||||
|
||||
|
||||
// ─── all() ──────────────────────────────────────────────────────────────
|
||||
// all() -> table of rows
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
@@ -376,8 +366,28 @@ static void register_default_query_methods(lua_State *L,
|
||||
return run_sql_query(L, ss.str());
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "all");
|
||||
}
|
||||
|
||||
lua_pushstring(L, tablename.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
const char *tbl = lua_tostring(L, lua_upvalueindex(1));
|
||||
std::stringstream ss;
|
||||
ss << "SELECT COUNT(*) AS c FROM " << tbl << " ";
|
||||
lua_getfield(L, 1, "__filter_sql");
|
||||
if (lua_isstring(L, -1)) ss << lua_tostring(L, -1) << " ";
|
||||
lua_pop(L, 1);
|
||||
ss << ";";
|
||||
run_sql_query(L, ss.str()); // [{ c = "N" }]
|
||||
lua_rawgeti(L, -1, 1);
|
||||
lua_getfield(L, -1, "c");
|
||||
const char *cs = lua_tostring(L, -1);
|
||||
lua_Integer n = cs ? (lua_Integer) std::strtoll(cs, nullptr, 10) : 0;
|
||||
lua_pop(L, 3); // c, row, result
|
||||
lua_pushinteger(L, n);
|
||||
return 1;
|
||||
}, 1);
|
||||
lua_setfield(L, idx, "count");
|
||||
}
|
||||
|
||||
static int model_new(lua_State *L)
|
||||
{
|
||||
@@ -403,7 +413,7 @@ static int model_new(lua_State *L)
|
||||
|
||||
static void create_model_table(lua_State *L, const LumeniteDB::Model &M)
|
||||
{
|
||||
// instance metatable (for User.new)
|
||||
// instance metatable (for User.new instances)
|
||||
std::string mtn = "LumeniteDB.instance." + M.tablename;
|
||||
luaL_newmetatable(L, mtn.c_str());
|
||||
lua_pushstring(L, M.tablename.c_str());
|
||||
@@ -421,7 +431,7 @@ static void create_model_table(lua_State *L, const LumeniteDB::Model &M)
|
||||
lua_pushcclosure(L, model_new, 1);
|
||||
lua_setfield(L, md, "new");
|
||||
|
||||
// column helpers (User.name:asc(), :desc())
|
||||
// column helpers (User.name:asc() / :desc())
|
||||
for (auto &c: M.columns) {
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, c.name.c_str());
|
||||
@@ -432,7 +442,6 @@ static void create_model_table(lua_State *L, const LumeniteDB::Model &M)
|
||||
return 1;
|
||||
}, 1);
|
||||
lua_setfield(L, -2, "asc");
|
||||
|
||||
lua_pushstring(L, c.name.c_str());
|
||||
lua_pushcclosure(L, [](lua_State *L)-> int
|
||||
{
|
||||
@@ -441,7 +450,6 @@ static void create_model_table(lua_State *L, const LumeniteDB::Model &M)
|
||||
return 1;
|
||||
}, 1);
|
||||
lua_setfield(L, -2, "desc");
|
||||
|
||||
lua_setfield(L, md, c.name.c_str());
|
||||
}
|
||||
|
||||
@@ -451,7 +459,6 @@ static void create_model_table(lua_State *L, const LumeniteDB::Model &M)
|
||||
lua_setfield(L, md, "query");
|
||||
}
|
||||
|
||||
|
||||
// Catches foo.name = "bar" and queues an UPDATE
|
||||
static int instance_newindex(lua_State *L)
|
||||
{
|
||||
@@ -468,8 +475,8 @@ static int instance_newindex(lua_State *L)
|
||||
lua_pop(L, 1);
|
||||
|
||||
LumeniteDB::Update upd;
|
||||
upd.tablename = tn;
|
||||
upd.changes["id"] = id;
|
||||
upd.tablename = tn ? tn : "";
|
||||
upd.changes["id"] = id ? id : "";
|
||||
lua_pushvalue(L, 3);
|
||||
const char *v = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
@@ -479,7 +486,8 @@ static int instance_newindex(lua_State *L)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// DB impl
|
||||
bool LumeniteDB::DB::open(const std::string &f)
|
||||
{
|
||||
return sqlite3_open(f.c_str(), &handle) == SQLITE_OK;
|
||||
@@ -511,7 +519,8 @@ LumeniteDB::DB **LumeniteDB::check(lua_State *L)
|
||||
return static_cast<DB **>(luaL_checkudata(L, 1, "LumeniteDB.DB"));
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Lua API core
|
||||
int LumeniteDB::db_open(lua_State *L)
|
||||
{
|
||||
const char *fn = luaL_checkstring(L, 1);
|
||||
@@ -524,7 +533,7 @@ int LumeniteDB::db_open(lua_State *L)
|
||||
if (!fs::exists(logdir)) fs::create_directories(logdir);
|
||||
|
||||
fs::path full = dbdir / fn;
|
||||
auto **ud = static_cast<DB **>(lua_newuserdata(L, sizeof(DB*)));
|
||||
auto **ud = static_cast<DB **>(lua_newuserdata(L, sizeof(DB *)));
|
||||
*ud = new DB();
|
||||
luaL_getmetatable(L, "LumeniteDB.DB");
|
||||
lua_setmetatable(L, -2);
|
||||
@@ -535,48 +544,54 @@ int LumeniteDB::db_open(lua_State *L)
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Enable foreign keys
|
||||
db_instance = *ud;
|
||||
run_sql_exec(L, "PRAGMA foreign_keys = ON;");
|
||||
|
||||
fs::path logfile = logdir / (db_filename + ".log");
|
||||
sql_log_stream.open(logfile, std::ios::app);
|
||||
if (!sql_log_stream) {
|
||||
std::cerr << "Warning: could not open SQL log at "
|
||||
<< logfile.string() << "\n";
|
||||
std::cerr << "Warning: could not open SQL log at " << logfile.string() << "\n";
|
||||
}
|
||||
|
||||
db_instance = *ud;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int LumeniteDB::db_gc(lua_State *L)
|
||||
{
|
||||
delete *check(L);
|
||||
DB **ud = check(L);
|
||||
if (ud && *ud) {
|
||||
if (db_instance == *ud) db_instance = nullptr;
|
||||
delete *ud;
|
||||
*ud = nullptr;
|
||||
}
|
||||
if (sql_log_stream.is_open()) {
|
||||
sql_log_stream.flush();
|
||||
sql_log_stream.close();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int LumeniteDB::db_column(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const char *type = luaL_checkstring(L, 2);
|
||||
|
||||
bool pk = false;
|
||||
std::string defv; // will hold whatever `options.default` was
|
||||
std::string defv;
|
||||
|
||||
if (lua_istable(L, 3)) {
|
||||
// primary_key?
|
||||
lua_getfield(L, 3, "primary_key");
|
||||
pk = lua_toboolean(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
// default?
|
||||
lua_getfield(L, 3, "default");
|
||||
if (lua_isstring(L, -1) || lua_isnumber(L, -1)) {
|
||||
defv = lua_tostring(L, -1); // number→string or string
|
||||
defv = lua_tostring(L, -1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// return { name=…, type=…, primary_key=…, default_value=… }
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, name);
|
||||
lua_setfield(L, -2, "name");
|
||||
@@ -585,7 +600,7 @@ int LumeniteDB::db_column(lua_State *L)
|
||||
lua_pushboolean(L, pk);
|
||||
lua_setfield(L, -2, "primary_key");
|
||||
lua_pushstring(L, defv.c_str());
|
||||
lua_setfield(L, -2, "default_value"); // <— changed key here
|
||||
lua_setfield(L, -2, "default_value");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -595,7 +610,6 @@ int LumeniteDB::db_model(lua_State *L)
|
||||
if (lua_gettop(L) >= 2 && lua_istable(L, 2)) d = 2;
|
||||
luaL_checktype(L, d, LUA_TTABLE);
|
||||
|
||||
// __tablename
|
||||
lua_getfield(L, d, "__tablename");
|
||||
if (!lua_isstring(L, -1))
|
||||
return luaL_error(L, "db.Model: missing '__tablename'");
|
||||
@@ -635,11 +649,10 @@ int LumeniteDB::db_model(lua_State *L)
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int LumeniteDB::db_create_all(lua_State *L)
|
||||
{
|
||||
for (auto it = models.rbegin(); it != models.rend(); ++it) {
|
||||
const auto &[tn,mdl] = *it;
|
||||
const auto &[tn, mdl] = *it;
|
||||
std::stringstream ss;
|
||||
ss << "CREATE TABLE IF NOT EXISTS " << tn << " (";
|
||||
for (size_t i = 0; i < mdl.columns.size(); ++i) {
|
||||
@@ -661,78 +674,148 @@ int LumeniteDB::db_create_all(lua_State *L)
|
||||
|
||||
int LumeniteDB::db_session_add(lua_State *L)
|
||||
{
|
||||
luaL_checktype(L, 1,LUA_TTABLE);
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
|
||||
Row row;
|
||||
// Copy all k/v to row.values
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, 1)) {
|
||||
row.values[lua_tostring(L, -2)] = lua_tostring(L, -1) ? lua_tostring(L, -1) : "";
|
||||
const char *k = lua_tostring(L, -2);
|
||||
const char *v = lua_tostring(L, -1);
|
||||
row.values[k ? k : ""] = v ? v : "";
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// Resolve model/tablname from metatable.__model
|
||||
lua_getmetatable(L, 1);
|
||||
lua_getfield(L, -1, "__model");
|
||||
session.tablename = lua_tostring(L, -1);
|
||||
const char *tn = lua_tostring(L, -1);
|
||||
lua_pop(L, 2);
|
||||
|
||||
// Record table per-row without changing the public structs
|
||||
row.values[LM_HIDDEN_TABLE_KEY] = tn ? tn : "";
|
||||
|
||||
// Keep session.tablename for backward-compat (single-table sessions)
|
||||
session.tablename = tn ? tn : "";
|
||||
|
||||
session.pending_inserts.push_back(std::move(row));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int LumeniteDB::db_session_commit(lua_State *L)
|
||||
{
|
||||
// INSERTs
|
||||
require_db(L);
|
||||
|
||||
// INSERTs (prepared)
|
||||
for (auto &r: session.pending_inserts) {
|
||||
std::stringstream k, v;
|
||||
k << "(";
|
||||
v << "(";
|
||||
bool first = true;
|
||||
for (auto &kv: r.values) {
|
||||
if (!first) {
|
||||
k << ", ";
|
||||
v << ", ";
|
||||
}
|
||||
first = false;
|
||||
k << kv.first;
|
||||
v << "'" << kv.second << "'";
|
||||
auto itT = r.values.find(LM_HIDDEN_TABLE_KEY);
|
||||
std::string tablename = (itT != r.values.end()) ? itT->second : session.tablename;
|
||||
|
||||
// Build ordered list of columns/values excluding the hidden key
|
||||
std::vector<std::string> cols;
|
||||
std::vector<std::string> vals;
|
||||
cols.reserve(r.values.size());
|
||||
vals.reserve(r.values.size());
|
||||
|
||||
for (const auto &kv: r.values) {
|
||||
if (kv.first == LM_HIDDEN_TABLE_KEY) continue;
|
||||
cols.push_back(kv.first);
|
||||
vals.push_back(kv.second);
|
||||
}
|
||||
k << ")";
|
||||
v << ")";
|
||||
|
||||
// build and print the INSERT SQL
|
||||
std::string sql =
|
||||
"INSERT INTO " + session.tablename +
|
||||
" " + k.str() + " VALUES " + v.str() + ";";
|
||||
if (cols.empty()) continue;
|
||||
|
||||
run_sql_exec(L, sql);
|
||||
std::stringstream ss;
|
||||
ss << "INSERT INTO " << tablename << " (";
|
||||
for (size_t i = 0; i < cols.size(); ++i) {
|
||||
if (i) ss << ", ";
|
||||
ss << cols[i];
|
||||
}
|
||||
ss << ") VALUES (";
|
||||
for (size_t i = 0; i < cols.size(); ++i) {
|
||||
if (i) ss << ", ";
|
||||
ss << "?";
|
||||
}
|
||||
ss << ");";
|
||||
|
||||
std::string sql = ss.str();
|
||||
log_sql(sql);
|
||||
|
||||
sqlite3_stmt *stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_instance->handle, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
luaL_error(L, "SQLite prepare failed (INSERT): %s", sqlite3_errmsg(db_instance->handle));
|
||||
}
|
||||
|
||||
for (int i = 0; i < (int) vals.size(); ++i) {
|
||||
sqlite3_bind_text(stmt, i + 1, vals[i].c_str(), -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
luaL_error(L, "SQLite step failed (INSERT): %s", sqlite3_errmsg(db_instance->handle));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
session.pending_inserts.clear();
|
||||
|
||||
// UPDATEs
|
||||
// UPDATEs (prepared)
|
||||
for (auto &u: session.pending_updates) {
|
||||
auto itId = u.changes.find("id");
|
||||
if (itId == u.changes.end()) {
|
||||
continue; // no id -> skip
|
||||
}
|
||||
std::string idVal = itId->second;
|
||||
|
||||
std::vector<std::pair<std::string, std::string> > sets;
|
||||
sets.reserve(u.changes.size());
|
||||
for (const auto &kv: u.changes) {
|
||||
if (kv.first == "id") continue;
|
||||
sets.push_back(kv);
|
||||
}
|
||||
if (sets.empty()) continue;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "UPDATE " << u.tablename << " SET ";
|
||||
bool first = true;
|
||||
for (auto &cv: u.changes) {
|
||||
if (cv.first == "id") continue;
|
||||
if (!first) ss << ", ";
|
||||
first = false;
|
||||
ss << cv.first << "='" << cv.second << "'";
|
||||
for (size_t i = 0; i < sets.size(); ++i) {
|
||||
if (i) ss << ", ";
|
||||
ss << sets[i].first << " = ?";
|
||||
}
|
||||
ss << " WHERE id='" << u.changes["id"] << "';";
|
||||
ss << " WHERE id = ?;";
|
||||
|
||||
// build and print the UPDATE SQL
|
||||
std::string sql = ss.str();
|
||||
log_sql(sql);
|
||||
|
||||
run_sql_exec(L, sql);
|
||||
sqlite3_stmt *stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_instance->handle, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
luaL_error(L, "SQLite prepare failed (UPDATE): %s", sqlite3_errmsg(db_instance->handle));
|
||||
}
|
||||
|
||||
int idx = 1;
|
||||
for (const auto &kv: sets) {
|
||||
sqlite3_bind_text(stmt, idx++, kv.second.c_str(), -1, SQLITE_TRANSIENT);
|
||||
}
|
||||
sqlite3_bind_text(stmt, idx, idVal.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
sqlite3_finalize(stmt);
|
||||
luaL_error(L, "SQLite step failed (UPDATE): %s", sqlite3_errmsg(db_instance->handle));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
session.pending_updates.clear();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int LumeniteDB::db_select_all(lua_State *L)
|
||||
{
|
||||
require_db(L);
|
||||
const char *tn = luaL_checkstring(L, 1);
|
||||
std::string q = std::string("SELECT * FROM ") + tn + ";";
|
||||
log_sql(q);
|
||||
|
||||
sqlite3_stmt *s = nullptr;
|
||||
if (sqlite3_prepare_v2(db_instance->handle, q.c_str(), -1, &s, nullptr) != SQLITE_OK)
|
||||
return luaL_error(L, "SQLite prepare failed: %s", sqlite3_errmsg(db_instance->handle));
|
||||
@@ -744,23 +827,78 @@ int LumeniteDB::db_select_all(lua_State *L)
|
||||
for (int c = 0; c < sqlite3_column_count(s); ++c) {
|
||||
const char *col = sqlite3_column_name(s, c);
|
||||
const unsigned char *txt = sqlite3_column_text(s, c);
|
||||
if (txt) lua_pushstring(L, (const char *) txt);
|
||||
if (txt) lua_pushstring(L, reinterpret_cast<const char *>(txt));
|
||||
else lua_pushnil(L);
|
||||
lua_setfield(L, -2, col);
|
||||
}
|
||||
|
||||
lua_rawseti(L, -2, i++);
|
||||
}
|
||||
sqlite3_finalize(s);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Extras: transactions, last_insert_id, delete
|
||||
int LumeniteDB::db_begin(lua_State *L)
|
||||
{
|
||||
run_sql_exec(L, "BEGIN;");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int LumeniteDB::db_commit(lua_State *L)
|
||||
{
|
||||
run_sql_exec(L, "COMMIT;");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int LumeniteDB::db_rollback(lua_State *L)
|
||||
{
|
||||
run_sql_exec(L, "ROLLBACK;");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int LumeniteDB::db_last_id(lua_State *L)
|
||||
{
|
||||
require_db(L);
|
||||
lua_pushinteger(L, (lua_Integer) sqlite3_last_insert_rowid(db_instance->handle));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LumeniteDB::db_delete(lua_State *L)
|
||||
{
|
||||
require_db(L);
|
||||
const char *tn = luaL_checkstring(L, 1);
|
||||
luaL_argcheck(L, lua_isinteger(L, 2) || lua_isstring(L, 2), 2, "id must be int or string");
|
||||
|
||||
std::string sql = std::string("DELETE FROM ") + tn + " WHERE id = ?;";
|
||||
log_sql(sql);
|
||||
|
||||
sqlite3_stmt *st = nullptr;
|
||||
if (sqlite3_prepare_v2(db_instance->handle, sql.c_str(), -1, &st, nullptr) != SQLITE_OK)
|
||||
return luaL_error(L, "SQLite prepare failed (DELETE): %s", sqlite3_errmsg(db_instance->handle));
|
||||
|
||||
if (lua_isinteger(L, 2)) sqlite3_bind_int64(st, 1, (sqlite3_int64) lua_tointeger(L, 2));
|
||||
else sqlite3_bind_text(st, 1, lua_tostring(L, 2), -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_DONE) {
|
||||
sqlite3_finalize(st);
|
||||
return luaL_error(L, "SQLite step failed (DELETE): %s", sqlite3_errmsg(db_instance->handle));
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
extern "C" int luaopen_lumenite_db(lua_State *L)
|
||||
{
|
||||
#ifndef LUMENITE_DB_NO_BANNER
|
||||
std::cout << YELLOW << "[~] Notice : " << RESET
|
||||
<< "The module " << BOLD << "'lumenite.db'" << RESET
|
||||
<< " is currently in " << BOLD RED << "Alpha" << RESET << ".\n"
|
||||
<< " Use with caution - it may be incomplete or insecure.\n";
|
||||
#endif
|
||||
|
||||
luaL_newmetatable(L, "LumeniteDB.DB");
|
||||
lua_pushcfunction(L, LumeniteDB::db_gc);
|
||||
@@ -782,5 +920,18 @@ extern "C" int luaopen_lumenite_db(lua_State *L)
|
||||
lua_setfield(L, -2, "session_commit");
|
||||
lua_pushcfunction(L, LumeniteDB::db_select_all);
|
||||
lua_setfield(L, -2, "select_all");
|
||||
|
||||
// extras
|
||||
lua_pushcfunction(L, LumeniteDB::db_begin);
|
||||
lua_setfield(L, -2, "begin");
|
||||
lua_pushcfunction(L, LumeniteDB::db_commit);
|
||||
lua_setfield(L, -2, "commit");
|
||||
lua_pushcfunction(L, LumeniteDB::db_rollback);
|
||||
lua_setfield(L, -2, "rollback");
|
||||
lua_pushcfunction(L, LumeniteDB::db_last_id);
|
||||
lua_setfield(L, -2, "last_insert_id");
|
||||
lua_pushcfunction(L, LumeniteDB::db_delete);
|
||||
lua_setfield(L, -2, "delete");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,17 @@ public:
|
||||
|
||||
static int db_select_all(lua_State *L);
|
||||
|
||||
static int db_begin(lua_State *L);
|
||||
|
||||
static int db_commit(lua_State *L);
|
||||
|
||||
static int db_rollback(lua_State *L);
|
||||
|
||||
static int db_last_id(lua_State *L);
|
||||
|
||||
static int db_delete(lua_State *L);
|
||||
|
||||
|
||||
static int db_gc(lua_State *L);
|
||||
|
||||
static DB **check(lua_State *L);
|
||||
|
||||
@@ -320,73 +320,97 @@ end)
|
||||
)");
|
||||
|
||||
writeFile("app/models.lua", R"(-- app/models.lua
|
||||
|
||||
---@diagnostic disable: undefined-global
|
||||
local db = require("lumenite.db")
|
||||
|
||||
--[[
|
||||
Model definitions for Lumenite.
|
||||
Use this file to register and configure your application's database models.
|
||||
|
||||
• db.open(filename) - Open (or create) the SQLite database under ./db/
|
||||
• db.Column(name, type, opts) - Declare a column (opts.primary_key = true)
|
||||
• db.Model{…} - Define a new model/table
|
||||
• db.create_all() - Create all tables you've defined
|
||||
• model.new(data) - Instantiate a row for insertion
|
||||
• model.query - Built‑in query API with methods:
|
||||
• :get(id) - Fetch a single row by primary key
|
||||
• :all() - Fetch all matching rows
|
||||
• :first() - Fetch the first matching row
|
||||
• :filter_by{…} - Filter rows by a set of conditions
|
||||
• :order_by(expr)- Order by a set of conditions
|
||||
• db.session_add(row) - Stage a row for insertion
|
||||
• db.session_commit() - Commit all staged inserts
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
-- 1) Open (or create) the SQLite file under ./db/
|
||||
-- The engine ensures ./db and ./log exist and enables PRAGMA foreign_keys.
|
||||
local conn, err = db.open("user.db")
|
||||
assert(conn, "db.open failed: " .. tostring(err))
|
||||
|
||||
-- 2) define a simple User model
|
||||
local User = db.Model {
|
||||
__tablename = "users",
|
||||
id = db.Column("id", "INTEGER", { primary_key = true }),
|
||||
name = db.Column("name", "TEXT"),
|
||||
created_at = db.Column("created_at", "TEXT", { default = os.time() })
|
||||
-- 2) Define models
|
||||
-- Tip: Use INTEGER for primary keys. SQLite will back it by rowid.
|
||||
local User = db.Model{
|
||||
__tablename = "users",
|
||||
id = db.Column("id", "INTEGER", { primary_key = true }),
|
||||
name = db.Column("name", "TEXT"),
|
||||
created_at = db.Column("created_at", "INTEGER", { default = os.time() }),
|
||||
}
|
||||
|
||||
-- 3) create the table
|
||||
-- 3) Create tables if they don’t exist
|
||||
db.create_all()
|
||||
|
||||
-- 4) insert a few rows
|
||||
for _, name in ipairs { "Alice", "Bob", "Charlie" } do
|
||||
local u = User.new { name = name }
|
||||
db.session_add(u)
|
||||
end
|
||||
db.session_commit()
|
||||
|
||||
-- 5) select_all test
|
||||
local all = db.select_all("users")
|
||||
print("All users:")
|
||||
for i, row in ipairs(all) do
|
||||
print(i, row.id, row.name)
|
||||
-- 4) Seed data (only if empty)
|
||||
if (User.query:count() == 0) then
|
||||
for _, name in ipairs({ "Alice", "Bob", "Charlie" }) do
|
||||
db.session_add(User.new{ name = name })
|
||||
end
|
||||
db.session_commit()
|
||||
end
|
||||
|
||||
-- 6) query.filter_by + .all()
|
||||
local alices = User.query:filter_by({ name = "Alice" }):all()
|
||||
assert(#alices == 1, "Expected exactly one Alice")
|
||||
print("Queried Alice -> id=" .. alices[1].id)
|
||||
-- 5) Example: select_all (plain tables; values are strings or nil)
|
||||
do
|
||||
local all = db.select_all("users")
|
||||
print("All users:")
|
||||
for i, row in ipairs(all) do
|
||||
print(i, row.id, row.name, row.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
-- 7) query:get(id)
|
||||
local bob = User.query:get(2)
|
||||
assert(bob and bob.name == "Bob", "Expected Bob at id=2")
|
||||
print("User.get(2) -> name=" .. bob.name)
|
||||
-- 6) Query API examples (chainable; executes on :all/:first/:get/:count)
|
||||
do
|
||||
local alices = User.query:filter_by{ name = "Alice" }:all()
|
||||
assert(#alices >= 1, "Expected at least one Alice")
|
||||
print("Queried Alice -> id=" .. alices[1].id)
|
||||
|
||||
-- 8) query:first() with order_by
|
||||
local last = User.query:order_by(User.name:desc()):first()
|
||||
assert(last, "Last is nil")
|
||||
print("First by name DESC ->", last.id, last.name)
|
||||
local bob = User.query:get(2) -- returns proxy or nil
|
||||
if bob then
|
||||
print("User.get(2) -> name=" .. bob.name)
|
||||
end
|
||||
|
||||
local last = User.query:order_by(User.name:desc()):first()
|
||||
if last then
|
||||
print("First by name DESC ->", last.id, last.name)
|
||||
end
|
||||
end
|
||||
|
||||
-- 7) Updates are queued on the proxy, then applied on db.session_commit()
|
||||
do
|
||||
local u = User.query:filter_by{ name = "Charlie" }:first()
|
||||
if u then
|
||||
u.name = "Charlene" -- queued UPDATE
|
||||
db.session_commit() -- apply UPDATE
|
||||
print("Updated user id=" .. u.id .. " -> name=" .. (User.query:get(u.id).name))
|
||||
end
|
||||
end
|
||||
|
||||
-- 8) Transactions + last_insert_id()
|
||||
do
|
||||
db.begin()
|
||||
db.session_add(User.new{ name = "Dave" })
|
||||
db.session_commit() -- insert happens within transaction
|
||||
local new_id = db.last_insert_id()
|
||||
db.commit()
|
||||
print("Inserted Dave with id=" .. tostring(new_id))
|
||||
end
|
||||
|
||||
-- 9) Delete by id (prepared)
|
||||
-- Uncomment to try:
|
||||
-- do
|
||||
-- local eve = User.query:filter_by{ name = "Eve" }:first()
|
||||
-- if eve then
|
||||
-- db.delete("users", eve.id)
|
||||
-- print("Deleted user id=" .. eve.id)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
-- Export models + db so the app can require them
|
||||
return {
|
||||
db = db,
|
||||
User = User,
|
||||
}
|
||||
|
||||
print("All tests passed!")
|
||||
|
||||
|
||||
|
||||
@@ -486,70 +510,113 @@ print("All tests passed!")
|
||||
---@module "lumenite.db"
|
||||
local db = {}
|
||||
|
||||
--[[!!
|
||||
Lumenite DB — Lua API (EmmyLua annotations)
|
||||
-------------------------------------------
|
||||
• All row values returned by query/all/select_all are strings (SQLite text) or nil.
|
||||
• Query methods are chainable and do not execute until :first(), :all(), :get(), or :count().
|
||||
• :get() and :first() return a *proxy* table; reading fields reads current values, assigning
|
||||
(e.g., proxy.name = "X") queues an UPDATE applied on db.session_commit().
|
||||
• INTEGER PRIMARY KEY columns are recommended for ids (rowid).
|
||||
• Defaults: when you pass `options.default` to Column(...), CREATE TABLE will include a DEFAULT
|
||||
literal (numeric unquoted, strings quoted).
|
||||
!!]]
|
||||
|
||||
---@alias ColumnOptions { primary_key?: boolean, default?: any }
|
||||
|
||||
---@alias ColumnDef { name: string, type: string, primary_key: boolean }
|
||||
---@class ColumnDef
|
||||
---@field name string
|
||||
---@field type string
|
||||
---@field primary_key boolean
|
||||
---@field default_value string @empty string if unset (stringified literal for DDL)
|
||||
|
||||
---@class ColumnHelper
|
||||
---@field asc fun(self: ColumnHelper): string @“<col> ASC”
|
||||
---@field desc fun(self: ColumnHelper): string @“<col> DESC”
|
||||
---@field asc fun(self: ColumnHelper): string @returns "<col> ASC"
|
||||
---@field desc fun(self: ColumnHelper): string @returns "<col> DESC"
|
||||
|
||||
---@class QueryTable
|
||||
---@field filter_by fun(self: QueryTable, filters: { [string]: string|number }): QueryTable @add a WHERE clause
|
||||
---@field order_by fun(self: QueryTable, expr: string): QueryTable @add an ORDER BY clause
|
||||
---@field limit fun(self: QueryTable, n: integer): QueryTable @limit results
|
||||
---@field get fun(self: QueryTable, id: string|integer): table? @fetch one by id
|
||||
---@field first fun(self: QueryTable): table? @fetch first match
|
||||
---@field all fun(self: QueryTable): table[] @fetch all matches
|
||||
---@field filter_by fun(self: QueryTable, filters: { [string]: string|number|boolean|nil }): QueryTable
|
||||
---@field order_by fun(self: QueryTable, expr: string): QueryTable
|
||||
---@field limit fun(self: QueryTable, n: integer): QueryTable
|
||||
---@field get fun(self: QueryTable, id: string|integer): table? @proxy row or nil
|
||||
---@field first fun(self: QueryTable): table? @proxy row or nil
|
||||
---@field all fun(self: QueryTable): table[] @array of plain row tables
|
||||
---@field count fun(self: QueryTable): integer @row count for current filters
|
||||
|
||||
---@class ModelTable
|
||||
---@field new fun(def: { [string]: any }): table @create a new instance
|
||||
---@field query QueryTable @the query API
|
||||
---@field new fun(def: { [string]: any }): table @creates a new instance (to be inserted)
|
||||
---@field query QueryTable @chainable query builder
|
||||
---@field [string] ColumnHelper @each column name → helper with :asc()/:desc()
|
||||
|
||||
---@class DB
|
||||
---@field open fun(filename: string): DB?, string? @open/create `./db/filename`
|
||||
---@field Column fun(name: string, type: string, options?: ColumnOptions): ColumnDef
|
||||
---@field Model fun(def: { __tablename: string, [string]: ColumnDef }): ModelTable
|
||||
---@field create_all fun(): nil @CREATE TABLE IF NOT EXISTS …
|
||||
---@field session_add fun(row: table): nil @stage an insert
|
||||
---@field session_commit fun(): nil @commit staged inserts
|
||||
---@field select_all fun(tablename: string): table[] @SELECT * FROM tablename
|
||||
---@field open fun(filename: string): DB?, string? @open/create `./db/<filename>`
|
||||
---@field Column fun(name: string, type: string, options?: ColumnOptions): ColumnDef
|
||||
---@field Model fun(def: { __tablename: string, [string]: ColumnDef }): ModelTable
|
||||
---@field create_all fun(): nil
|
||||
---@field session_add fun(row: table): nil @stage an INSERT (from Model.new)
|
||||
---@field session_commit fun(): nil @apply staged INSERTs/UPDATEs
|
||||
---@field select_all fun(tablename: string): table[] @SELECT * FROM <tablename>
|
||||
---@field begin fun(): nil @BEGIN transaction
|
||||
---@field commit fun(): nil @COMMIT transaction
|
||||
---@field rollback fun(): nil @ROLLBACK transaction
|
||||
---@field last_insert_id fun(): integer @sqlite3_last_insert_rowid()
|
||||
---@field delete fun(tablename: string, id: string|integer): nil @DELETE FROM <table> WHERE id=?
|
||||
|
||||
--- Opens (or creates) a SQLite file under `./db/`
|
||||
--- Opens (or creates) a SQLite file under `./db/`.
|
||||
--- Also ensures `./db` and `./log` folders exist and enables `PRAGMA foreign_keys = ON`.
|
||||
---@param filename string
|
||||
---@return DB?, string? — the DB instance or nil+error
|
||||
---@return DB? db, string? err -- the DB instance or nil+error
|
||||
function db.open(filename) end
|
||||
|
||||
--- Defines a new column descriptor
|
||||
--- Defines a new column descriptor for use in db.Model.
|
||||
--- If options.default is numeric, it's emitted unquoted; strings are quoted in DDL.
|
||||
---@param name string
|
||||
---@param type string
|
||||
---@param options? ColumnOptions
|
||||
---@return ColumnDef
|
||||
function db.Column(name, type, options) end
|
||||
|
||||
--- Defines a new model
|
||||
--- Defines a new model/table. Example:
|
||||
--- local User = db.Model{ __tablename="users", id=db.Column("id","INTEGER",{primary_key=true}) }
|
||||
---@param def { __tablename: string, [string]: ColumnDef }
|
||||
---@return ModelTable
|
||||
function db.Model(def) end
|
||||
|
||||
--- Creates all defined tables
|
||||
--- Creates all registered tables with CREATE TABLE IF NOT EXISTS.
|
||||
function db.create_all() end
|
||||
|
||||
--- Stage a row for insertion
|
||||
--- Stage a row for insertion (from Model.new{...}). Applied on db.session_commit().
|
||||
---@param row table
|
||||
function db.session_add(row) end
|
||||
|
||||
--- Commit all staged inserts
|
||||
--- Apply all staged INSERTs and queued UPDATEs (from proxy assignments).
|
||||
function db.session_commit() end
|
||||
|
||||
--- Select * from a table
|
||||
--- Values are strings or nil.
|
||||
---@param tablename string
|
||||
---@return table[]
|
||||
function db.select_all(tablename) end
|
||||
|
||||
--- BEGIN a transaction.
|
||||
function db.begin() end
|
||||
|
||||
--- COMMIT the current transaction.
|
||||
function db.commit() end
|
||||
|
||||
--- ROLLBACK the current transaction.
|
||||
function db.rollback() end
|
||||
|
||||
--- Returns sqlite3_last_insert_rowid() of the current connection.
|
||||
---@return integer
|
||||
function db.last_insert_id() end
|
||||
|
||||
---@param tablename string
|
||||
---@param id string|integer
|
||||
function db.delete(tablename, id) end
|
||||
|
||||
return db
|
||||
|
||||
|
||||
)");
|
||||
|
||||
writeFile(".lumenite/__syntax__.lua", R"(
|
||||
|
||||
5
test/Test-Project/.gitignore
vendored
Normal file
5
test/Test-Project/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
*.db
|
||||
*.log
|
||||
.vscode/
|
||||
build/
|
||||
120
test/Test-Project/.lumenite/__syntax__.lua
Normal file
120
test/Test-Project/.lumenite/__syntax__.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
---@meta
|
||||
|
||||
--[[----------------------------------------------------------------------------
|
||||
This file provides IntelliSense and type annotations for the Lumenite web framework.
|
||||
|
||||
DO NOT EDIT THIS FILE MANUALLY.
|
||||
It is automatically generated and used by Lua language servers (such as EmmyLua / LuaLS)
|
||||
to enable autocompletion, documentation, and static type checking in Lumenite-based apps.
|
||||
|
||||
Any manual changes will be overwritten during regeneration or update.
|
||||
------------------------------------------------------------------------------]]
|
||||
|
||||
---@alias Headers table<string, string>
|
||||
---@alias RouteHandler fun(req: Request, ...: string): string|Response|table
|
||||
|
||||
|
||||
|
||||
---@class SendFileOptions
|
||||
---@field as_attachment? boolean
|
||||
---@field download_name? string
|
||||
---@field content_type? string
|
||||
---@field status? integer
|
||||
---@field headers? Headers
|
||||
|
||||
---@class Request
|
||||
---@field method string
|
||||
---@field path string
|
||||
---@field headers Headers
|
||||
---@field query table<string, string|string[]>
|
||||
---@field form table<string, string|string[]>
|
||||
---@field body string
|
||||
---@field remote_ip string
|
||||
|
||||
---@class Response
|
||||
---@field status integer
|
||||
---@field headers Headers
|
||||
---@field body string
|
||||
|
||||
---@class App
|
||||
local app = {}
|
||||
|
||||
---@param path string
|
||||
---@param handler RouteHandler
|
||||
function app:get(path, handler) end
|
||||
|
||||
---@param path string
|
||||
---@param handler RouteHandler
|
||||
function app:post(path, handler) end
|
||||
|
||||
---@param path string
|
||||
---@param handler RouteHandler
|
||||
function app:put(path, handler) end
|
||||
|
||||
---@param path string
|
||||
---@param handler RouteHandler
|
||||
function app:delete(path, handler) end
|
||||
|
||||
---@param key string
|
||||
---@return string
|
||||
function app.session_get(key) end
|
||||
|
||||
---@param key string
|
||||
---@param value string
|
||||
function app.session_set(key, value) end
|
||||
|
||||
---@param name string
|
||||
---@param fn fun(input: string): string
|
||||
function app:template_filter(name, fn) end
|
||||
|
||||
---@param filename string
|
||||
---@param context table
|
||||
---@return string
|
||||
function app.render_template(filename, context) end
|
||||
|
||||
---@param template_string string
|
||||
---@param context table
|
||||
---@return string
|
||||
function app.render_template_string(template_string, context) end
|
||||
|
||||
---@param path string
|
||||
---@param options? SendFileOptions
|
||||
---@return Response
|
||||
function app.send_file(path, options) end
|
||||
|
||||
---@param table table
|
||||
---@return Response
|
||||
function app.jsonify(table) end
|
||||
|
||||
---@param json string
|
||||
---@return table
|
||||
function app.json(json) end
|
||||
|
||||
---@param json string
|
||||
---@return table
|
||||
function app.from_json(json) end
|
||||
|
||||
---@param fn fun(req: Request): Response|nil
|
||||
function app.before_request(fn) end
|
||||
|
||||
---@param fn fun(req: Request, res: Response): Response|nil
|
||||
function app.after_request(fn) end
|
||||
|
||||
---@param url string
|
||||
---@return table
|
||||
function app.http_get(url) end
|
||||
|
||||
---@overload fun(status: integer)
|
||||
---@param status integer
|
||||
---@param message? string
|
||||
function app.abort(status, message) end
|
||||
|
||||
---@param port integer
|
||||
function app:listen(port) end
|
||||
|
||||
---@type App
|
||||
_G.app = app
|
||||
|
||||
return app
|
||||
|
||||
109
test/Test-Project/.lumenite/db.lua
Normal file
109
test/Test-Project/.lumenite/db.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
---@meta
|
||||
---@module "lumenite.db"
|
||||
local db = {}
|
||||
|
||||
--[[!!
|
||||
Lumenite DB — Lua API (EmmyLua annotations)
|
||||
-------------------------------------------
|
||||
• All row values returned by query/all/select_all are strings (SQLite text) or nil.
|
||||
• Query methods are chainable and do not execute until :first(), :all(), :get(), or :count().
|
||||
• :get() and :first() return a *proxy* table; reading fields reads current values, assigning
|
||||
(e.g., proxy.name = "X") queues an UPDATE applied on db.session_commit().
|
||||
• INTEGER PRIMARY KEY columns are recommended for ids (rowid).
|
||||
• Defaults: when you pass `options.default` to Column(...), CREATE TABLE will include a DEFAULT
|
||||
literal (numeric unquoted, strings quoted).
|
||||
!!]]
|
||||
|
||||
---@alias ColumnOptions { primary_key?: boolean, default?: any }
|
||||
|
||||
---@class ColumnDef
|
||||
---@field name string
|
||||
---@field type string
|
||||
---@field primary_key boolean
|
||||
---@field default_value string @empty string if unset (stringified literal for DDL)
|
||||
|
||||
---@class ColumnHelper
|
||||
---@field asc fun(self: ColumnHelper): string @returns "<col> ASC"
|
||||
---@field desc fun(self: ColumnHelper): string @returns "<col> DESC"
|
||||
|
||||
---@class QueryTable
|
||||
---@field filter_by fun(self: QueryTable, filters: { [string]: string|number|boolean|nil }): QueryTable
|
||||
---@field order_by fun(self: QueryTable, expr: string): QueryTable
|
||||
---@field limit fun(self: QueryTable, n: integer): QueryTable
|
||||
---@field get fun(self: QueryTable, id: string|integer): table? @proxy row or nil
|
||||
---@field first fun(self: QueryTable): table? @proxy row or nil
|
||||
---@field all fun(self: QueryTable): table[] @array of plain row tables
|
||||
---@field count fun(self: QueryTable): integer @row count for current filters
|
||||
|
||||
---@class ModelTable
|
||||
---@field new fun(def: { [string]: any }): table @creates a new instance (to be inserted)
|
||||
---@field query QueryTable @chainable query builder
|
||||
---@field [string] ColumnHelper @each column name → helper with :asc()/:desc()
|
||||
|
||||
---@class DB
|
||||
---@field open fun(filename: string): DB?, string? @open/create `./db/<filename>`
|
||||
---@field Column fun(name: string, type: string, options?: ColumnOptions): ColumnDef
|
||||
---@field Model fun(def: { __tablename: string, [string]: ColumnDef }): ModelTable
|
||||
---@field create_all fun(): nil
|
||||
---@field session_add fun(row: table): nil @stage an INSERT (from Model.new)
|
||||
---@field session_commit fun(): nil @apply staged INSERTs/UPDATEs
|
||||
---@field select_all fun(tablename: string): table[] @SELECT * FROM <tablename>
|
||||
---@field begin fun(): nil @BEGIN transaction
|
||||
---@field commit fun(): nil @COMMIT transaction
|
||||
---@field rollback fun(): nil @ROLLBACK transaction
|
||||
---@field last_insert_id fun(): integer @sqlite3_last_insert_rowid()
|
||||
---@field delete fun(tablename: string, id: string|integer): nil @DELETE FROM <table> WHERE id=?
|
||||
|
||||
--- Opens (or creates) a SQLite file under `./db/`.
|
||||
--- Also ensures `./db` and `./log` folders exist and enables `PRAGMA foreign_keys = ON`.
|
||||
---@param filename string
|
||||
---@return DB? db, string? err -- the DB instance or nil+error
|
||||
function db.open(filename) end
|
||||
|
||||
--- Defines a new column descriptor for use in db.Model.
|
||||
--- If options.default is numeric, it's emitted unquoted; strings are quoted in DDL.
|
||||
---@param name string
|
||||
---@param type string
|
||||
---@param options? ColumnOptions
|
||||
---@return ColumnDef
|
||||
function db.Column(name, type, options) end
|
||||
|
||||
--- Defines a new model/table. Example:
|
||||
--- local User = db.Model{ __tablename="users", id=db.Column("id","INTEGER",{primary_key=true}) }
|
||||
---@param def { __tablename: string, [string]: ColumnDef }
|
||||
---@return ModelTable
|
||||
function db.Model(def) end
|
||||
|
||||
--- Creates all registered tables with CREATE TABLE IF NOT EXISTS.
|
||||
function db.create_all() end
|
||||
|
||||
--- Stage a row for insertion (from Model.new{...}). Applied on db.session_commit().
|
||||
---@param row table
|
||||
function db.session_add(row) end
|
||||
|
||||
--- Apply all staged INSERTs and queued UPDATEs (from proxy assignments).
|
||||
function db.session_commit() end
|
||||
|
||||
--- Values are strings or nil.
|
||||
---@param tablename string
|
||||
---@return table[]
|
||||
function db.select_all(tablename) end
|
||||
|
||||
--- BEGIN a transaction.
|
||||
function db.begin() end
|
||||
|
||||
--- COMMIT the current transaction.
|
||||
function db.commit() end
|
||||
|
||||
--- ROLLBACK the current transaction.
|
||||
function db.rollback() end
|
||||
|
||||
--- Returns sqlite3_last_insert_rowid() of the current connection.
|
||||
---@return integer
|
||||
function db.last_insert_id() end
|
||||
|
||||
---@param tablename string
|
||||
---@param id string|integer
|
||||
function db.delete(tablename, id) end
|
||||
|
||||
return db
|
||||
3
test/Test-Project/README.md
Normal file
3
test/Test-Project/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Test-Project
|
||||
|
||||
Made by [Lumenite](https://github.com/OusmBlueNinja/Lumenite)
|
||||
27
test/Test-Project/app.lua
Normal file
27
test/Test-Project/app.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
-- app.lua
|
||||
|
||||
--[[
|
||||
Lumenite Entry Point
|
||||
|
||||
This is your main application bootstrap file.
|
||||
It loads route handlers, middleware, filters, and models.
|
||||
|
||||
Each file in the `app/` folder encapsulates a part of your app:
|
||||
- filters.lua → defines custom template filters
|
||||
- middleware.lua → defines pre- and post-request logic
|
||||
- routes.lua → defines HTTP route handlers
|
||||
- models.lua → defines database models (ORM)
|
||||
|
||||
You can customize the port or add environment setup here.
|
||||
This file is the first thing run by the Lumenite engine.
|
||||
--]]
|
||||
|
||||
require("app.models")
|
||||
require("app.filters")
|
||||
require("app.middleware")
|
||||
|
||||
require("app.routes.web")
|
||||
require("app.routes.api")
|
||||
|
||||
app:listen(8080)
|
||||
|
||||
32
test/Test-Project/app/filters.lua
Normal file
32
test/Test-Project/app/filters.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
-- app/filters.lua
|
||||
local safe = require("lumenite.safe")
|
||||
|
||||
--[[
|
||||
Template Filters
|
||||
|
||||
This file defines custom filters available in your templates.
|
||||
|
||||
Filters allow you to transform data inside templates:
|
||||
Example usage in template.html:
|
||||
{{ title | upper }} -- convert title to uppercase
|
||||
{{ content | safe }} -- mark content as safe HTML
|
||||
|
||||
Defining a filter:
|
||||
app:template_filter("name", function(input)
|
||||
-- do something with input
|
||||
return result
|
||||
end)
|
||||
|
||||
This example defines a 'safe' filter using the Lumenite Safe module,
|
||||
which escapes HTML to prevent XSS vulnerabilities.
|
||||
|
||||
You can add more filters here, like:
|
||||
"truncate", "markdown", "date_format", etc.
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
app:template_filter("safe", function(input)
|
||||
return safe.escape(input)
|
||||
end)
|
||||
|
||||
27
test/Test-Project/app/middleware.lua
Normal file
27
test/Test-Project/app/middleware.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
-- app/middleware.lua
|
||||
local models = require("app.models")
|
||||
|
||||
--[[
|
||||
Middleware configuration for Lumenite.
|
||||
Use this file to register hooks that run before or after each request.
|
||||
|
||||
- app.before_request(fn): Called before every route
|
||||
- app.after_request(fn): Called after every route
|
||||
|
||||
Example use cases:
|
||||
• Logging
|
||||
• Authentication
|
||||
• Header manipulation
|
||||
--]]
|
||||
|
||||
app.before_request(function(req)
|
||||
-- Example: log the User-Agent
|
||||
-- print(req.headers["User-Agent"])
|
||||
end)
|
||||
|
||||
app.after_request(function(request, response)
|
||||
response.headers["X-Powered-By"] = "Lumenite"
|
||||
return response
|
||||
end)
|
||||
|
||||
|
||||
71
test/Test-Project/app/models.lua
Normal file
71
test/Test-Project/app/models.lua
Normal file
@@ -0,0 +1,71 @@
|
||||
-- app/models.lua
|
||||
local db = require("lumenite.db")
|
||||
|
||||
--[[
|
||||
Model definitions for Lumenite.
|
||||
Use this file to register and configure your application's database models.
|
||||
|
||||
• db.open(filename) - Open (or create) the SQLite database under ./db/
|
||||
• db.Column(name, type, opts) - Declare a column (opts.primary_key = true)
|
||||
• db.Model{…} - Define a new model/table
|
||||
• db.create_all() - Create all tables you've defined
|
||||
• model.new(data) - Instantiate a row for insertion
|
||||
• model.query - Built‑in query API with methods:
|
||||
• :get(id) - Fetch a single row by primary key
|
||||
• :all() - Fetch all matching rows
|
||||
• :first() - Fetch the first matching row
|
||||
• :filter_by{…} - Filter rows by a set of conditions
|
||||
• :order_by(expr)- Order by a set of conditions
|
||||
• db.session_add(row) - Stage a row for insertion
|
||||
• db.session_commit() - Commit all staged inserts
|
||||
--]]
|
||||
|
||||
|
||||
|
||||
local conn, err = db.open("user.db")
|
||||
assert(conn, "db.open failed: " .. tostring(err))
|
||||
|
||||
-- 2) define a simple User model
|
||||
local User = db.Model {
|
||||
__tablename = "users",
|
||||
id = db.Column("id", "INTEGER", { primary_key = true }),
|
||||
name = db.Column("name", "TEXT"),
|
||||
created_at = db.Column("created_at", "TEXT", { default = os.time() })
|
||||
}
|
||||
|
||||
-- 3) create the table
|
||||
db.create_all()
|
||||
|
||||
-- 4) insert a few rows
|
||||
for _, name in ipairs { "Alice", "Bob", "Charlie" } do
|
||||
local u = User.new { name = name }
|
||||
db.session_add(u)
|
||||
end
|
||||
db.session_commit()
|
||||
|
||||
-- 5) select_all test
|
||||
local all = db.select_all("users")
|
||||
print("All users:")
|
||||
for i, row in ipairs(all) do
|
||||
print(i, row.id, row.name)
|
||||
end
|
||||
|
||||
-- 6) query.filter_by + .all()
|
||||
local alices = User.query:filter_by({ name = "Alice" }):all()
|
||||
assert(#alices == 1, "Expected exactly one Alice")
|
||||
print("Queried Alice -> id=" .. alices[1].id)
|
||||
|
||||
-- 7) query:get(id)
|
||||
local bob = User.query:get(2)
|
||||
assert(bob and bob.name == "Bob", "Expected Bob at id=2")
|
||||
print("User.get(2) -> name=" .. bob.name)
|
||||
|
||||
-- 8) query:first() with order_by
|
||||
local last = User.query:order_by(User.name:desc()):first()
|
||||
assert(last, "Last is nil")
|
||||
print("First by name DESC ->", last.id, last.name)
|
||||
|
||||
print("All tests passed!")
|
||||
|
||||
|
||||
|
||||
22
test/Test-Project/app/routes/api.lua
Normal file
22
test/Test-Project/app/routes/api.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- app/routes/api.lua
|
||||
local models = require("app.models")
|
||||
|
||||
--[[
|
||||
API Routes
|
||||
|
||||
Define routes that return JSON responses (REST-style).
|
||||
These are typically used by client apps or JavaScript.
|
||||
|
||||
You can define routes using:
|
||||
app:get(path, handler)
|
||||
app:post(path, handler)
|
||||
--]]
|
||||
|
||||
app:get("/api/ping", function(request)
|
||||
return app.jsonify({
|
||||
status = "ok",
|
||||
time = os.date("!%Y-%m-%d %H:%M:%S UTC"),
|
||||
headers = request.headers
|
||||
})
|
||||
end)
|
||||
|
||||
23
test/Test-Project/app/routes/web.lua
Normal file
23
test/Test-Project/app/routes/web.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
-- app/routes/web.lua
|
||||
local crypto = require("lumenite.crypto")
|
||||
local models = require("app.models")
|
||||
|
||||
--[[
|
||||
Web Routes
|
||||
|
||||
Define routes that render HTML views or templates.
|
||||
These are typically used for browser-facing endpoints.
|
||||
|
||||
You can define routes using:
|
||||
app:get(path, handler)
|
||||
app:post(path, handler)
|
||||
--]]
|
||||
|
||||
app:get("/", function(request)
|
||||
return app.render_template("template.html", {
|
||||
title = "Welcome to Lumenite",
|
||||
project_name = "Test-Project",
|
||||
content = "<p>This content was injected into the layout.</p>",
|
||||
timestamp = os.date("!%Y-%m-%d %H:%M:%S UTC")
|
||||
})
|
||||
end)
|
||||
2
test/Test-Project/config.luma
Normal file
2
test/Test-Project/config.luma
Normal file
@@ -0,0 +1,2 @@
|
||||
project_name: Test-Project
|
||||
lumenite_version: 2025.5
|
||||
BIN
test/Test-Project/db/test_lmdb.sqlite
Normal file
BIN
test/Test-Project/db/test_lmdb.sqlite
Normal file
Binary file not shown.
2
test/Test-Project/plugins/modules.cpl
Normal file
2
test/Test-Project/plugins/modules.cpl
Normal file
@@ -0,0 +1,2 @@
|
||||
# Lumenite Plugins
|
||||
plugins: []
|
||||
0
test/Test-Project/static/javascript/index.js
Normal file
0
test/Test-Project/static/javascript/index.js
Normal file
0
test/Test-Project/static/styles/style.css
Normal file
0
test/Test-Project/static/styles/style.css
Normal file
80
test/Test-Project/templates/template.html
Normal file
80
test/Test-Project/templates/template.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{{ title }}</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(to bottom right, #1e1e2f, #2c2c3e);
|
||||
color: #f2f2f2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #2a2a3a;
|
||||
padding: 1.5rem 2rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #444;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #a8d0ff;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
font-size: 0.85rem;
|
||||
background-color: #1b1b2b;
|
||||
color: #aaa;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.powered {
|
||||
margin-top: 0.5rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #77bbee;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>{{ project_name }}</header>
|
||||
<main>
|
||||
<h2>{{ title }}</h2>
|
||||
{{ content }}
|
||||
</main>
|
||||
<footer>
|
||||
<div><em>Rendered at {{ timestamp }}</em></div>
|
||||
<div class="powered">Powered by <a href="https://github.com/OusmBlueNinja/Lumenite" target="_blank">Lumenite</a></div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,71 +1,77 @@
|
||||
-- app/models.lua
|
||||
local db = require("lumenite.db")
|
||||
|
||||
--[[
|
||||
Model definitions for Lumenite.
|
||||
Use this file to register and configure your application's database models.
|
||||
|
||||
• db.open(filename) - Open (or create) the SQLite database under ./db/
|
||||
• db.Column(name, type, opts) - Declare a column (opts.primary_key = true)
|
||||
• db.Model{…} - Define a new model/table
|
||||
• db.create_all() - Create all tables you've defined
|
||||
• model.new(data) - Instantiate a row for insertion
|
||||
• model.query - Built‑in query API with methods:
|
||||
• :get(id) - Fetch a single row by primary key
|
||||
• :all() - Fetch all matching rows
|
||||
• :first() - Fetch the first matching row
|
||||
• :filter_by{…} - Filter rows by a set of conditions
|
||||
• :order_by(expr)- Order by a set of conditions
|
||||
• db.session_add(row) - Stage a row for insertion
|
||||
• db.session_commit() - Commit all staged inserts
|
||||
--]]
|
||||
|
||||
|
||||
-- fresh start for dev/testing
|
||||
local function try_remove(path) pcall(os.remove, path) end
|
||||
try_remove("db/user.db")
|
||||
try_remove("log/user.db.log")
|
||||
|
||||
-- open/create DB under ./db/
|
||||
local conn, err = db.open("user.db")
|
||||
assert(conn, "db.open failed: " .. tostring(err))
|
||||
|
||||
-- 2) define a simple User model
|
||||
local User = db.Model {
|
||||
-- Model: users
|
||||
-- Note: keep created_at as INTEGER with default os.time() so CREATE TABLE uses a numeric DEFAULT
|
||||
local User = db.Model{
|
||||
__tablename = "users",
|
||||
id = db.Column("id", "INTEGER", { primary_key = true }),
|
||||
name = db.Column("name", "TEXT"),
|
||||
created_at = db.Column("created_at", "TEXT", { default = os.time() })
|
||||
id = db.Column("id", "INTEGER", { primary_key = true }), -- INTEGER PRIMARY KEY ⇒ rowid
|
||||
name = db.Column("name", "TEXT"),
|
||||
created_at = db.Column("created_at", "INTEGER", { default = os.time() })
|
||||
}
|
||||
|
||||
-- 3) create the table
|
||||
-- create tables
|
||||
db.create_all()
|
||||
|
||||
-- 4) insert a few rows
|
||||
for _, name in ipairs { "Alice", "Bob", "Charlie" } do
|
||||
local u = User.new { name = name }
|
||||
db.session_add(u)
|
||||
-- seed rows (id will auto-assign because INTEGER PRIMARY KEY)
|
||||
for _, name in ipairs({ "Alice", "Bob", "Charlie" }) do
|
||||
db.session_add(User.new{ name = name })
|
||||
end
|
||||
db.session_commit()
|
||||
|
||||
-- 5) select_all test
|
||||
local all = db.select_all("users")
|
||||
print("All users:")
|
||||
for i, row in ipairs(all) do
|
||||
print(i, row.id, row.name)
|
||||
-- print all
|
||||
local function print_rows(tag, rows)
|
||||
print(tag)
|
||||
for i, r in ipairs(rows) do
|
||||
print(i, r.id, r.name, r.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
-- 6) query.filter_by + .all()
|
||||
local alices = User.query:filter_by({ name = "Alice" }):all()
|
||||
assert(#alices == 1, "Expected exactly one Alice")
|
||||
-- select_all sanity
|
||||
local all = db.select_all("users")
|
||||
assert(#all == 3, "expected 3 users after seed")
|
||||
print_rows("All users:", all)
|
||||
|
||||
-- filter_by + all()
|
||||
local alices = User.query:filter_by{ name = "Alice" }:all()
|
||||
assert(#alices == 1, "expected exactly one Alice")
|
||||
print("Queried Alice -> id=" .. alices[1].id)
|
||||
|
||||
-- 7) query:get(id)
|
||||
-- get(id) (note: ids come back as strings from SQLite text fetch)
|
||||
local bob = User.query:get(2)
|
||||
assert(bob and bob.name == "Bob", "Expected Bob at id=2")
|
||||
assert(bob and bob.name == "Bob", "expected Bob at id=2")
|
||||
print("User.get(2) -> name=" .. bob.name)
|
||||
|
||||
-- 8) query:first() with order_by
|
||||
-- first() with order_by(desc)
|
||||
local last = User.query:order_by(User.name:desc()):first()
|
||||
assert(last, "Last is nil")
|
||||
assert(last ~= nil, "first() returned nil unexpectedly")
|
||||
print("First by name DESC ->", last.id, last.name)
|
||||
|
||||
-- limit() + order_by()
|
||||
local top2 = User.query:order_by(User.id:asc()):limit(2):all()
|
||||
assert(#top2 == 2 and top2[1].name == "Alice" and top2[2].name == "Bob", "limit/order_by failed")
|
||||
print_rows("Top 2 by id asc:", top2)
|
||||
|
||||
-- proxy update flow (queues UPDATE; apply on commit)
|
||||
local c = User.query:get(3) -- Charlie
|
||||
assert(c and c.name == "Charlie", "expected Charlie at id=3")
|
||||
c.name = "Charlene" -- queued update
|
||||
db.session_commit() -- applies
|
||||
local c2 = User.query:get(3)
|
||||
assert(c2.name == "Charlene", "proxy update did not persist")
|
||||
print("Updated id=3 ->", c2.name)
|
||||
|
||||
-- get() / first() nil behavior on missing rows
|
||||
assert(User.query:get(999) == nil, "get(999) should be nil")
|
||||
assert(User.query:filter_by{ name = "Nobody" }:first() == nil, "first() on empty should be nil")
|
||||
|
||||
print("All tests passed!")
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user