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:
2025-08-09 04:05:44 -05:00
parent 888b2d8997
commit d2d49a0c4a
20 changed files with 1050 additions and 292 deletions

View File

@@ -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)
{
// Callers 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 slot1 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 WHEREfragment 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 1element 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;
}

View File

@@ -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);

View File

@@ -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 - Builtin 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 dont 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
View File

@@ -0,0 +1,5 @@
*.db
*.log
.vscode/
build/

View 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

View 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

View File

@@ -0,0 +1,3 @@
# Test-Project
Made by [Lumenite](https://github.com/OusmBlueNinja/Lumenite)

27
test/Test-Project/app.lua Normal file
View 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)

View 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)

View 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)

View 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 - Builtin 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!")

View 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)

View 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)

View File

@@ -0,0 +1,2 @@
project_name: Test-Project
lumenite_version: 2025.5

Binary file not shown.

View File

@@ -0,0 +1,2 @@
# Lumenite Plugins
plugins: []

View 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>

View File

@@ -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 - Builtin 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!")