Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,49 @@ While it's pre-1.0, minor versions may carry breaking changes.

## [Unreleased]

## [2.0.0] - 2026-08-26

The one intentional breaking bump. The config object changes shape and the factory gets a new
name — both batched here, while the install base is small, so consumers migrate once.

### Changed

- **Config declares both engines and selects the active one** (breaking): `DALConfig.db` is no
longer a `SqliteConfig | PostgresConfig` union with a `mode` discriminant on each block. It is
now a plain selector — `db: 'local' | 'cloud'` — with the two engine configs declared alongside
it under `local` and `cloud`:

```ts
// before (1.x)
await db.connect({ db: { mode: 'local', dataDir: './data' } });
// after (2.0)
await db.connect({ db: 'local', local: { dataDir: './data' } });
```

Every declared block is validated at `connect()`, so a bad cloud `connectionString` is caught at
boot while you're still on local — not the first time you flip to cloud in production. The `mode`
field is gone from `SqliteConfig`/`PostgresConfig` (the selector replaces it). A `db: 'cloud'`
with no `cloud` block throws `ConfigurationError`.

### Added

- **`sqlSwitch()`** is the factory's new name (matches the package, fits the `express()`/`fastify()`
package-as-factory convention). `createDAL` stays as a `@deprecated` alias — one line, no second
code path — and will be removed in 3.0. The default export is `sqlSwitch`, so
`import sqlSwitch from 'sql-switch'` needs no change.
- **`db.reconnect(target?)`**: one primitive for restart / recover-a-wedged-connection / repoint.
No argument re-opens the current engine; `'local'`/`'cloud'` switches to the other declared engine
**without moving data** (that's still `swapEngine()`). Flushes pending writes first, and is
fail-safe — the target engine is built and validated before the current one is torn down, so a bad
target leaves you on the engine you had.

### Migration

- `db: { mode: 'local', ...rest }` → `db: 'local', local: { ...rest }`.
- `db: { mode: 'cloud', connectionString, pool }` → `db: 'cloud', cloud: { connectionString, pool }`.
- Optionally declare both blocks and drive `db` from an env var (`db: process.env.DB_MODE`).
- `createDAL()` keeps working; rename to `sqlSwitch()` at your leisure before 3.0.

## [1.0.1] - 2026-08-21

Tier 1 correctness fixes (packaging + Postgres value integrity + write durability).
Expand Down Expand Up @@ -104,7 +147,8 @@ Initial pre-release of the universal SQLite/PostgreSQL DAL.
crashing, then recovers.
- Bidirectional engine swap => migrate data SQLite files <=> PostgreSQL schemas.

[Unreleased]: https://github.com/creative-softworks/sql-switch/compare/v1.0.1...HEAD
[Unreleased]: https://github.com/creative-softworks/sql-switch/compare/v2.0.0...HEAD
[2.0.0]: https://github.com/creative-softworks/sql-switch/compare/v1.0.1...v2.0.0
[1.0.1]: https://github.com/creative-softworks/sql-switch/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/creative-softworks/sql-switch/compare/v0.2.0...v1.0.0
[0.2.0]: https://github.com/creative-softworks/sql-switch/compare/v0.1.0...v0.2.0
Expand Down
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ npm install sql-switch better-sqlite3
```

```ts
import { createDAL } from 'sql-switch';
import { sqlSwitch } from 'sql-switch';

const db = createDAL();
const db = sqlSwitch();

await db.connect({
db: { mode: 'local', dataDir: './data/databases', wal: true },
db: 'local',
local: { dataDir: './data/databases', wal: true },
collector: { enabled: true, time: 3000 },
});

Expand All @@ -46,19 +47,25 @@ That is the whole surface: `schema → table → key → operation`. The same ch

## Same code, production engine

Nothing above changes when you go to PostgreSQL — only the `connect()` config does:
Declare **both** engines up front and pick the active one with `db` — an env var is the usual
selector. Only the config changes; not one line of your data code does:

```ts
await db.connect({
db: {
mode: 'cloud',
db: process.env.DB_MODE ?? 'local', // 'local' | 'cloud'
local: { dataDir: './data/databases', wal: true },
cloud: {
connectionString: process.env.DATABASE_URL,
pool: { max: 5, statementTimeout: 30_000 },
},
collector: { enabled: true, time: 3000 },
});
```

Both declared blocks are validated at `connect()`, so a bad cloud `connectionString` is caught at
boot even while you're still running local. Flip engines at runtime — no data move, just repoint the
client — with `db.reconnect('cloud')`; use `db.swapEngine()` when the rows need to travel too.

In local mode each schema is its own `.db` file (`./data/databases/antinuke.db`, WAL on by default);
in cloud mode each schema is a Postgres logical schema (`antinuke.settings`). Your code never sees
the difference.
Expand Down Expand Up @@ -133,7 +140,8 @@ Defaults are chosen so nothing is silently lost. All of it is configurable.

```ts
await db.connect({
db: { mode: 'cloud', connectionString: process.env.DATABASE_URL },
db: 'cloud',
cloud: { connectionString: process.env.DATABASE_URL },
collector: {
time: 3000, // flush interval
autoRecover: true, // breaker heals itself after an outage
Expand Down Expand Up @@ -247,6 +255,7 @@ if (result.skippedNames.length) console.warn('left alone:', result.skippedNames)
| `table.startsWith(prefix)` | Sugar for `.entries({ prefix })`. Prefix is bound, never a pattern. |
| `table.count(opts?)` | Row count, done in the DB (rows never materialized). |
| `table.deleteAll(opts?)` | Delete every key (or just those under a prefix). |
| `db.reconnect(target?)` | Re-open the current engine (restart / recover a wedged connection), or repoint to the other declared engine (`'local'`/`'cloud'`) without moving data. Flushes first; fail-safe. |
| `db.swapEngine(options)` | Migrate to the other engine & reconnect on it. |
| `db.pendingWrites` | Number of writes currently buffered in the collector. |
| `db.close()` | Flush pending writes and close all connections. |
Expand Down
4 changes: 2 additions & 2 deletions assets/readme/architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions assets/readme/hero.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sql-switch",
"version": "1.0.1",
"version": "2.0.0",
"description": "Universal hot-swappable DAL — SQLite in dev, PostgreSQL in prod, same fluent API",
"type": "module",
"packageManager": "pnpm@10.26.1",
Expand Down
4 changes: 2 additions & 2 deletions scoped/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@creative-softworks/sql-switch",
"version": "1.0.1",
"version": "2.0.0",
"description": "Branded alias of sql-switch — a universal hot-swappable DAL (SQLite in dev, PostgreSQL in prod, same fluent API). Re-exports the sql-switch package unchanged.",
"type": "module",
"exports": {
Expand Down Expand Up @@ -46,6 +46,6 @@
"provenance": true
},
"dependencies": {
"sql-switch": "1.0.1"
"sql-switch": "2.0.0"
}
}
7 changes: 4 additions & 3 deletions scripts/smoke-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ async function main(): Promise<void> {
console.log('\n[1] connect + immediate write via .force()');
const db = createDAL();
await db.connect({
db: { mode: 'local', dataDir: TEST_DIR, wal: true },
db: 'local',
local: { dataDir: TEST_DIR, wal: true },
collector: { enabled: true, time: 300 },
});

Expand Down Expand Up @@ -120,7 +121,7 @@ async function main(): Promise<void> {
await db.close();

const db2 = createDAL();
await db2.connect({ db: { mode: 'local', dataDir: TEST_DIR }, collector: { enabled: false } });
await db2.connect({ db: 'local', local: { dataDir: TEST_DIR }, collector: { enabled: false } });
const survived = await db2
.schema('economy')
.table('balances')
Expand All @@ -142,7 +143,7 @@ async function main(): Promise<void> {

console.log('\n[11] unserializable values are rejected at the call site');
const db3 = createDAL();
await db3.connect({ db: { mode: 'local', dataDir: TEST_DIR }, collector: { enabled: false } });
await db3.connect({ db: 'local', local: { dataDir: TEST_DIR }, collector: { enabled: false } });

// circular => JSON.stringify would throw later, inside a flush where nobody sees it
const circular: Record<string, unknown> = { name: 'loop' };
Expand Down
14 changes: 9 additions & 5 deletions scripts/swap-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ async function main(): Promise<void> {
console.log('\n[1] seed local SQLite data');
const local = createDAL();
await local.connect({
db: { mode: 'local', dataDir: TEST_DIR, wal: true },
db: 'local',
local: { dataDir: TEST_DIR, wal: true },
collector: { enabled: false },
});

Expand Down Expand Up @@ -121,7 +122,8 @@ async function main(): Promise<void> {
console.log('\n[3] read the migrated data through the cloud driver');
const cloud = createDAL();
await cloud.connect({
db: { mode: 'cloud', connectionString: url },
db: 'cloud',
cloud: { connectionString: url },
collector: { enabled: false },
});
const snowflake = await cloud
Expand Down Expand Up @@ -149,7 +151,7 @@ async function main(): Promise<void> {
check('no leftover temp file', !fs.existsSync(`${TEST_DIR}/${SCHEMA}.db.tmp`));

const back = createDAL();
await back.connect({ db: { mode: 'local', dataDir: TEST_DIR }, collector: { enabled: false } });
await back.connect({ db: 'local', local: { dataDir: TEST_DIR }, collector: { enabled: false } });
const roundtrip = await back
.schema(SCHEMA)
.table(TABLE)
Expand Down Expand Up @@ -197,7 +199,8 @@ async function main(): Promise<void> {
console.log('\n[6] db.swapEngine() migrates & reconnects in place');
const hot = createDAL();
await hot.connect({
db: { mode: 'local', dataDir: TEST_DIR, wal: true },
db: 'local',
local: { dataDir: TEST_DIR, wal: true },
collector: { enabled: true, time: 300 },
});
// queued (not forced) => proves swapEngine flushes before touching the files
Expand Down Expand Up @@ -282,7 +285,8 @@ async function main(): Promise<void> {
// and the row that did come down reads back through a local DAL
const es4back = createDAL();
await es4back.connect({
db: { mode: 'local', dataDir: ES4_DIR },
db: 'local',
local: { dataDir: ES4_DIR },
collector: { enabled: false },
});
const mixValue = await es4back
Expand Down
Loading