Add MySQL support to Lib\Db via multiple simultaneous named connections
Redesigns Lib\Db from a single global connection to a config-driven map
of named connections (config['db_connections']), each independently
lazy-connected, each with its own optional migrations_dir and its own
schema_migrations table. A sidecar can use more than one connection in
the same request (e.g. Db::query(...) against SQLite alongside
Db::query(..., 'legacy') against MySQL) — sidecars have full access to
any Lib\ class, so nothing stops a request from needing two databases
at once, which the original single-driver spec didn't account for.
Db::query()/Db::connection() both default to the 'default' connection
name so the common single-database case is unchanged at the call site.
Supersedes the flat db_driver/db_path/db_migrations_dir keys shipped in
the SQLite groundwork commit (a3b9967) — no downstream consumers yet,
so no migration path needed.
db_connections needed one deliberate exception to the project's usual
shallow config-merge rule: merged one level deeper, by connection name,
so App/config.php adding a connection doesn't delete 'default'. Verified
end-to-end against a real local MariaDB instance running alongside the
existing SQLite connection, which caught a real ordering bug in the
initial merge implementation (capturing defaults after they'd already
been overwritten) before it shipped.
Closes the "MySQL support" backlog item in novaconium/ISSUES.md.
This commit is contained in:
@@ -130,34 +130,66 @@ management"); don't extend this class toward multi-user/session-based
|
||||
auth — that's a separate, larger feature that
|
||||
will replace it.
|
||||
|
||||
`Lib\Db` (`novaconium/lib/Db.php`) is the SQLite groundwork tracked in
|
||||
`novaconium/ISSUES.md` — a thin, no-ORM PDO wrapper, `Lib\` (not `App\`) so
|
||||
a project can override it via `App/lib/Db.php` like any other `Lib\` class.
|
||||
`Db::query(string $sql, array $params = [])` (prepare+execute) is the only
|
||||
query-running helper — never add a string-interpolation shortcut; see
|
||||
`Lib\Db` (`novaconium/lib/Db.php`) is the SQLite/MySQL groundwork tracked
|
||||
in `novaconium/ISSUES.md` — a thin, no-ORM PDO wrapper, `Lib\` (not `App\`)
|
||||
so a project can override it via `App/lib/Db.php` like any other `Lib\`
|
||||
class. It supports multiple, independently-configured, **simultaneously
|
||||
open** named connections (`config['db_connections']`, keyed by name) rather
|
||||
than one global connection — because sidecars are plain PHP with full
|
||||
access to any `Lib\` class, a single request can legitimately need more
|
||||
than one database at once (e.g. this site's own SQLite data alongside a
|
||||
MySQL connection to a legacy database). `Db::query(string $sql, array
|
||||
$params = [], string $connection = 'default')` (prepare+execute) is the
|
||||
only query-running helper — never add a string-interpolation shortcut; see
|
||||
`Lib\Input`'s doc-comment, which already commits this project to
|
||||
parameterized queries as the sole SQL-injection defense. Connection is
|
||||
lazy (opened on first `Db::query()`/`Db::connection()` call, same shape as
|
||||
`Lib\Csrf`'s lazy session start), and migrates automatically at that point:
|
||||
plain `.sql` files under `App/migrations/` (`config['db_migrations_dir']`),
|
||||
filename-ordered, tracked in an auto-created `schema_migrations` table, run
|
||||
once each. `novaconium/bin/migrate.php` runs the same migration step
|
||||
explicitly (e.g. from a deploy script) without serving a request first.
|
||||
**`config['db_path']` must stay outside both `public/` (would be
|
||||
web-accessible) and `novaconium/`** — unlike `cache_dir`/`contact-log.txt`,
|
||||
which are disposable, a SQLite file is data a project can't afford to lose,
|
||||
and `novaconium/` gets wholesale-replaced by the "Updating the framework"
|
||||
workflow (`/admin/docs/getting-started`: `rm -rf novaconium && cp -r
|
||||
<new-novaconium>`). The default (`data/novaconium.sqlite`) lives in a new
|
||||
top-level `data/` directory instead — project-owned like `App/`, gitignored
|
||||
per-file (`*.sqlite`/`-journal`/`-wal`/`-shm`, with a tracked `.gitkeep` so
|
||||
the directory exists in a fresh clone) rather than wholesale like
|
||||
parameterized queries as the sole SQL-injection defense. Each connection is
|
||||
lazy and independent (opened on first `Db::query()`/`Db::connection()` call
|
||||
naming it, same shape as `Lib\Csrf`'s lazy session start), and migrates
|
||||
automatically at that point: plain `.sql` files under that connection's own
|
||||
`migrations_dir`, filename-ordered, tracked in that connection's own
|
||||
auto-created `schema_migrations` table (each connection's applied
|
||||
migrations are independent of any other connection's), run once each.
|
||||
`novaconium/bin/migrate.php` loops every configured connection and runs the
|
||||
same migration step explicitly (e.g. from a deploy script) without serving
|
||||
a request first. Only `'sqlite'` and `'mysql'` drivers are implemented; a
|
||||
connection's `migrations_dir` is optional — omit it to never run migrations
|
||||
against that connection (e.g. a legacy database this project shouldn't
|
||||
manage schema for).
|
||||
|
||||
**`db_connections` is the one config key in the project that isn't plain
|
||||
shallow-merge** — `bootstrap.php`/`bin/*.php`'s usual `array_merge($config,
|
||||
$appConfig)` would let a project's `App/config.php` silently delete the
|
||||
framework's `default` connection just by adding a second named connection
|
||||
(a shallow merge replaces the whole key, it doesn't merge inside it). So
|
||||
`Lib\Db::config()` (and the copy of this logic duplicated in
|
||||
`bin/migrate.php`, same duplication precedent as the two-step config load
|
||||
already duplicated across `bootstrap.php`/`bin/clear-cache.php`) merges
|
||||
`db_connections` one level deeper, by connection name, **after** capturing
|
||||
the framework defaults — capture the defaults *before* the top-level
|
||||
`array_merge()` overwrites `$config['db_connections']`, not after, or the
|
||||
deeper merge silently operates on the already-overwritten value and the
|
||||
`default` connection vanishes anyway. (This exact bug was hit once while
|
||||
building this feature — verified by testing a real `App/config.php`
|
||||
override end-to-end, not just reading the code — so it's worth re-checking
|
||||
by hand if this logic is ever touched again.) See
|
||||
`/admin/docs/database` for the worked example.
|
||||
|
||||
**`config['db_connections']['default']['path']` must stay outside both
|
||||
`public/` (would be web-accessible) and `novaconium/`** — unlike
|
||||
`cache_dir`/`contact-log.txt`, which are disposable, a SQLite file is data a
|
||||
project can't afford to lose, and `novaconium/` gets wholesale-replaced by
|
||||
the "Updating the framework" workflow (`/admin/docs/getting-started`: `rm
|
||||
-rf novaconium && cp -r <new-novaconium>`). The default
|
||||
(`data/novaconium.sqlite`) lives in a new top-level `data/` directory
|
||||
instead — project-owned like `App/`, gitignored per-file
|
||||
(`*.sqlite`/`-journal`/`-wal`/`-shm`, with a tracked `.gitkeep` so the
|
||||
directory exists in a fresh clone) rather than wholesale like
|
||||
`public/cache/`, since a project might reasonably want other non-DB files
|
||||
there later. Only `App/migrations/` is scanned for now (the framework ships
|
||||
no core tables of its own yet) — if a future framework feature needs a
|
||||
shipped migration, extend this to the same App-over-novaconium two-root
|
||||
scan `Overlay.php` already does for pages/lib, don't invent a second
|
||||
mechanism.
|
||||
there later. Only `App/migrations/` (the framework default connection's
|
||||
`migrations_dir`) is scanned by default — the framework ships no core
|
||||
tables of its own yet — if a future framework feature needs a shipped
|
||||
migration, extend this to the same App-over-novaconium two-root scan
|
||||
`Overlay.php` already does for pages/lib, don't invent a second mechanism.
|
||||
|
||||
## Running it
|
||||
|
||||
|
||||
Reference in New Issue
Block a user