Add SQLite groundwork: Lib\Db, migrations, and a project-owned data dir
Lib\Db (novaconium/lib/Db.php) is a thin, no-ORM PDO wrapper — lazy-connect like Lib\Csrf, Db::query() as the only query-running helper (prepared statements only, no interpolation shortcut, per Lib\Input's existing security stance). Plain .sql migrations under App/migrations/, applied in filename order and tracked in an auto-created schema_migrations table, run automatically on first connection or via novaconium/bin/migrate.php. Data lives in a new top-level data/ directory rather than novaconium/ or public/ — outside public/ so it's never web-accessible, and outside novaconium/ since that directory gets wholesale-replaced by the "Updating the framework" workflow, which would otherwise destroy it on every update. New config keys: db_driver (only 'sqlite' implemented), db_path, db_migrations_dir. Documented at /admin/docs/database and in AGENTS.md. Closes the "SQLite groundwork" backlog item in novaconium/ISSUES.md.
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
<li><a class="icon-link" href="/admin/docs/forms">{{ icons.email() }}Forms</a></li>
|
||||
<li><a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Libraries</a></li>
|
||||
<li><a class="icon-link" href="/admin/docs/config">{{ icons.book() }}Configuration</a></li>
|
||||
<li><a class="icon-link" href="/admin/docs/database">{{ icons.book() }}Database</a></li>
|
||||
<li><a class="icon-link" href="/admin/docs/admin-auth">{{ icons.lock() }}Admin authentication</a></li>
|
||||
<li><a class="icon-link" href="/admin/docs/layouts">{{ icons.book() }}Layouts</a></li>
|
||||
<li><a class="icon-link" href="/admin/docs/caching">{{ icons.book() }}Static caching</a></li>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends 'admin/docs/_layout/layout.twig' %}
|
||||
|
||||
{% import '_layout/icons.twig' as icons %}
|
||||
|
||||
{% block title %}Database{% endblock %}
|
||||
|
||||
{% block description %}Lib\Db — a thin PDO/SQLite wrapper with a plain-SQL migration convention.{% endblock %}
|
||||
|
||||
{% block robots %}noindex, nofollow{% endblock %}
|
||||
|
||||
{% block docs_content %}
|
||||
<h1>Database</h1>
|
||||
|
||||
<p><code>Lib\Db</code> (<code>novaconium/lib/Db.php</code>) is the SQLite groundwork tracked in <code>novaconium/ISSUES.md</code> — a thin PDO wrapper plus a minimal migration runner, no ORM and no query builder, consistent with this project's no-Composer, no-build-step philosophy. It's a <a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Lib\</a> class like <code>Input</code>/<code>Csrf</code>/<code>Mailer</code>, so a project can override it entirely by dropping its own <code>App/lib/Db.php</code>.</p>
|
||||
|
||||
<h2>Using it</h2>
|
||||
|
||||
<pre><code>use Lib\Db;
|
||||
|
||||
$rows = Db::query('SELECT * FROM posts WHERE published = ?', [1])->fetchAll();</code></pre>
|
||||
|
||||
<p><code>Db::query(string $sql, array $params = [])</code> prepares and executes in one call, returning the <code>PDOStatement</code>. It's the only query-running helper this class exposes — there is deliberately no string-interpolation convenience method. <code>Db::connection()</code> returns the raw <code>PDO</code> instance for anything <code>query()</code> doesn't cover (transactions, <code>lastInsertId()</code>, etc.).</p>
|
||||
|
||||
<p><strong>Always use parameter binding, never string-concatenate values into SQL</strong> — the same rule <a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Lib\Input</a>'s own documentation already commits to: cleaning input is defense-in-depth against HTML/script injection, not SQL injection, and no string transform makes arbitrary input safe to concatenate into a query. Parameterized queries are the only real defense, so <code>Db</code> never grows an <code>sqlSafe()</code>-style shortcut.</p>
|
||||
|
||||
<p>The connection is opened lazily — nothing touches the database file or runs a migration until the first real call to <code>Db::query()</code> or <code>Db::connection()</code>, so a request that never needs the database never pays for it.</p>
|
||||
|
||||
<h2>Configuration</h2>
|
||||
|
||||
<pre><code><?php
|
||||
// App/config.php
|
||||
return [
|
||||
'db_driver' => 'sqlite',
|
||||
'db_path' => __DIR__ . '/../data/novaconium.sqlite',
|
||||
'db_migrations_dir' => __DIR__ . '/migrations',
|
||||
];</code></pre>
|
||||
|
||||
<p>Only <code>'sqlite'</code> is implemented for <code>db_driver</code> today — MySQL support is tracked as a separate item in <code>novaconium/ISSUES.md</code>, and the wrapper avoids SQLite-only SQL where a MySQL-compatible equivalent exists so that lands without a retrofit.</p>
|
||||
|
||||
<p><code>db_path</code> defaults to a top-level <code>data/</code> directory — a sibling of <code>App/</code>, <code>novaconium/</code>, and <code>public/</code>, not nested inside any of them. This is deliberate: it can't live under <code>public/</code> (would be directly web-accessible), and it can't live under <code>novaconium/</code> either, since <a class="icon-link" href="/admin/docs/getting-started">{{ icons.book() }}updating the framework</a> means overwriting that whole directory — anything persisted there would be destroyed by the next update. <code>data/</code> is project-owned, like <code>App/</code>, and untouched by a framework update. Its contents (<code>*.sqlite</code> and the SQLite journal/WAL/SHM sidecar files) are gitignored; only a <code>.gitkeep</code> is tracked so the directory exists in a fresh clone.</p>
|
||||
|
||||
<h2>Migrations</h2>
|
||||
|
||||
<p>Plain <code>.sql</code> files under <code>App/migrations/</code> (<code>db_migrations_dir</code>), applied in filename order — name them with a numeric prefix to control ordering:</p>
|
||||
|
||||
<pre><code>-- App/migrations/0001_create_posts.sql
|
||||
CREATE TABLE posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL
|
||||
);</code></pre>
|
||||
|
||||
<p>Each file is tracked by filename in a <code>schema_migrations</code> table (created automatically) and only ever run once. Migrations apply automatically the first time <code>Db::connection()</code> is called in a process — zero-config, the same "just works" philosophy as static caching — or explicitly, without serving a request first:</p>
|
||||
|
||||
<pre><code>php novaconium/bin/migrate.php</code></pre>
|
||||
|
||||
<p>Only <code>App/migrations/</code> is scanned — the framework itself ships no core tables yet, so there's no second <code>novaconium/migrations/</code> root to merge in. If a future framework feature needs a shipped migration (e.g. <a href="https://git.4lt.ca/4lt/novaconium/issues">admin login's user table</a>), this can extend to the same App-over-novaconium two-root scan used for pages and lib.</p>
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{% block title %}Docs{% endblock %}
|
||||
|
||||
{% block description %}Framework documentation: routing, sidecars, forms, libraries, layouts, caching, styling.{% endblock %}
|
||||
{% block description %}Framework documentation: routing, sidecars, forms, libraries, database, layouts, caching, styling.{% endblock %}
|
||||
|
||||
{% block robots %}noindex, nofollow{% endblock %}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<li><a class="icon-link" href="/admin/docs/forms">{{ icons.email() }}Forms</a> — building a custom form with a sidecar, using the novaconium validation/spam libraries.</li>
|
||||
<li><a class="icon-link" href="/admin/docs/libraries">{{ icons.book() }}Libraries</a> — plain PHP classes under <code>Lib\</code>.</li>
|
||||
<li><a class="icon-link" href="/admin/docs/config">{{ icons.book() }}Configuration</a> — override framework settings from <code>App/config.php</code>.</li>
|
||||
<li><a class="icon-link" href="/admin/docs/database">{{ icons.book() }}Database</a> — <code>Lib\Db</code>, a thin PDO/SQLite wrapper with a plain-SQL migration convention.</li>
|
||||
<li><a class="icon-link" href="/admin/docs/admin-auth">{{ icons.lock() }}Admin authentication</a> — gate <code>/admin/*</code> behind HTTP Basic Auth, reusable for any future admin page.</li>
|
||||
<li><a class="icon-link" href="/admin/docs/layouts">{{ icons.book() }}Layouts</a> — pages and layouts are overridable, just like <code>Lib\</code>.</li>
|
||||
<li><a class="icon-link" href="/admin/docs/caching">{{ icons.book() }}Static caching</a> — how sidecar-less pages get served as static HTML.</li>
|
||||
|
||||
Reference in New Issue
Block a user