latticesql 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -13
- package/dist/cli.js +199 -79
- package/dist/index.cjs +199 -79
- package/dist/index.d.cts +82 -47
- package/dist/index.d.ts +82 -47
- package/dist/index.js +199 -79
- package/package.json +2 -4
- package/dist/postgres-worker.cjs +0 -124
package/README.md
CHANGED
|
@@ -77,10 +77,10 @@ npm install latticesql
|
|
|
77
77
|
|
|
78
78
|
Requires **Node.js 18+**. The default backend is SQLite (`better-sqlite3`) — no external database process needed.
|
|
79
79
|
|
|
80
|
-
To use the Postgres backend (for Supabase, Neon, RDS, or any other Postgres-compatible database), install the optional
|
|
80
|
+
To use the Postgres backend (for Supabase, Neon, RDS, or any other Postgres-compatible database), install the optional dependency:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
npm install latticesql pg
|
|
83
|
+
npm install latticesql pg
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
Then pass a connection string instead of a file path:
|
|
@@ -2153,12 +2153,14 @@ The rest of the API — `define()`, `init()`, `query()`, `insert()`, `render()`,
|
|
|
2153
2153
|
|
|
2154
2154
|
### Postgres setup
|
|
2155
2155
|
|
|
2156
|
-
`PostgresAdapter` depends on `pg
|
|
2156
|
+
`PostgresAdapter` depends on `pg`, listed as an `optionalDependency` so SQLite-only consumers don't pay the install cost. Install it when you actually use Postgres:
|
|
2157
2157
|
|
|
2158
2158
|
```bash
|
|
2159
|
-
npm install pg
|
|
2159
|
+
npm install pg
|
|
2160
2160
|
```
|
|
2161
2161
|
|
|
2162
|
+
> **Migrating from `<= 1.9.x`?** `synckit` is no longer a dependency. Drop it from your install. The `dist/postgres-worker.cjs` file is also gone (it served the now-removed sync surface).
|
|
2163
|
+
|
|
2162
2164
|
Then point Lattice at any Postgres-compatible database that speaks the standard wire protocol on port 5432:
|
|
2163
2165
|
|
|
2164
2166
|
```ts
|
|
@@ -2166,16 +2168,13 @@ const lattice = new Lattice('postgres://user:pass@host:5432/db');
|
|
|
2166
2168
|
await lattice.init();
|
|
2167
2169
|
```
|
|
2168
2170
|
|
|
2169
|
-
**
|
|
2170
|
-
|
|
2171
|
-
- **Transaction-mode pooling** (e.g. PgBouncer transaction-mode, Supabase port `6543`) — recommended for `pg.Pool`-backed callers. The async surface (`runAsync`/`getAsync`/`allAsync`/`withClient`) is designed for transaction-mode: server-side prepared statements aren't kept across calls (because the upstream connection returns to the pool at `COMMIT`), and `prepareAsync` re-binds per call, so the prepared-statement-incompatibility caveat doesn't apply. Migrations are wrapped in `withClient(fn)` and acquire a transaction-scoped advisory lock so concurrent app boots serialize cleanly.
|
|
2172
|
-
- **Session-mode pooling** (e.g. PgBouncer session-mode, Supabase port `5432`) — required if you rely on the synckit-bridged sync surface (`adapter.run`/`adapter.prepare`) for transactional code. The synckit worker owns a single `pg.Client`, so session-mode preserves the per-connection guarantees that raw `BEGIN`/`COMMIT` calls assume.
|
|
2171
|
+
**Recommended pooler:** **transaction-mode** (e.g. PgBouncer transaction-mode, Supabase port `6543`). `PostgresAdapter` is native against `pg.Pool` and designed for transaction-mode: server-side prepared statements aren't kept across calls (because the upstream connection returns to the pool at `COMMIT`), and `prepareAsync` re-binds per call. Migrations are wrapped in `withClient(fn)` and acquire a transaction-scoped advisory lock so concurrent app boots serialize cleanly. `pool.max` is configurable via `PostgresAdapterOptions.poolSize` (default 10).
|
|
2173
2172
|
|
|
2174
|
-
**
|
|
2173
|
+
**Async-only on Postgres (since 1.10.0):**
|
|
2175
2174
|
|
|
2176
|
-
-
|
|
2177
|
-
-
|
|
2178
|
-
- **Transactional contract**: any code that issues `BEGIN`/`COMMIT` should use `withClient(fn)
|
|
2175
|
+
- The async surface (`runAsync` / `getAsync` / `allAsync` / `prepareAsync` / `introspectColumnsAsync` / `addColumnAsync` / `withClient`) is the *only* path that does work against Postgres. The synchronous methods (`run` / `get` / `all` / `prepare` / `introspectColumns` / `addColumn`) **throw** with a clear error pointing at the async equivalent. `pg.Pool` is fundamentally async; the previous synckit-bridged sync surface was a workaround that blocked the Node main thread on `Atomics.wait`, and it was removed in 1.10.0 once lattice core had migrated to async at every call site (1.9.0).
|
|
2176
|
+
- `SQLiteAdapter` keeps the sync surface as the authoritative path (better-sqlite3 is sync by design). Its async methods just wrap the sync calls in resolved Promises — the one-microtask cost is negligible relative to having a single cross-dialect code path.
|
|
2177
|
+
- **Transactional contract**: any code that issues `BEGIN`/`COMMIT` should use `withClient(fn)`. The pool checks out a single connection for the lifetime of `fn` and the `TxClient` handed to `fn` pins every query to that connection. Raw `adapter.runAsync('BEGIN')` is unsafe — different awaited calls land on different upstream connections under transaction-mode pooling.
|
|
2179
2178
|
|
|
2180
2179
|
```ts
|
|
2181
2180
|
// Recommended pattern for transactional writes
|
|
@@ -2202,7 +2201,9 @@ export interface StorageAdapter {
|
|
|
2202
2201
|
// core. Most application code never needs to read this.
|
|
2203
2202
|
readonly dialect: 'sqlite' | 'postgres';
|
|
2204
2203
|
|
|
2205
|
-
// Sync surface — required.
|
|
2204
|
+
// Sync surface — required by the interface. Sync-native backends like
|
|
2205
|
+
// SQLite implement it; async-native backends like Postgres (since 1.10.0)
|
|
2206
|
+
// throw with a helpful error pointing callers at the async equivalents.
|
|
2206
2207
|
run(sql: string, params?: unknown[]): void;
|
|
2207
2208
|
get(sql: string, params?: unknown[]): Row | undefined;
|
|
2208
2209
|
all(sql: string, params?: unknown[]): Row[];
|
|
@@ -2217,6 +2218,8 @@ export interface StorageAdapter {
|
|
|
2217
2218
|
getAsync?(sql: string, params?: unknown[]): Promise<Row | undefined>;
|
|
2218
2219
|
allAsync?(sql: string, params?: unknown[]): Promise<Row[]>;
|
|
2219
2220
|
prepareAsync?(sql: string): PreparedStatementAsync;
|
|
2221
|
+
introspectColumnsAsync?(table: string): Promise<string[]>;
|
|
2222
|
+
addColumnAsync?(table: string, column: string, typeSpec: string): Promise<void>;
|
|
2220
2223
|
withClient?<T>(fn: (tx: TxClient) => Promise<T>): Promise<T>;
|
|
2221
2224
|
}
|
|
2222
2225
|
```
|
package/dist/cli.js
CHANGED
|
@@ -415,6 +415,16 @@ async function getAsyncOrSync(adapter, sql, params) {
|
|
|
415
415
|
async function allAsyncOrSync(adapter, sql, params) {
|
|
416
416
|
return adapter.allAsync ? adapter.allAsync(sql, params) : adapter.all(sql, params);
|
|
417
417
|
}
|
|
418
|
+
async function introspectColumnsAsyncOrSync(adapter, table) {
|
|
419
|
+
return adapter.introspectColumnsAsync ? adapter.introspectColumnsAsync(table) : adapter.introspectColumns(table);
|
|
420
|
+
}
|
|
421
|
+
async function addColumnAsyncOrSync(adapter, table, column, typeSpec) {
|
|
422
|
+
if (adapter.addColumnAsync) {
|
|
423
|
+
await adapter.addColumnAsync(table, column, typeSpec);
|
|
424
|
+
} else {
|
|
425
|
+
adapter.addColumn(table, column, typeSpec);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
418
428
|
|
|
419
429
|
// src/db/sqlite.ts
|
|
420
430
|
import Database from "better-sqlite3";
|
|
@@ -483,6 +493,26 @@ var SQLiteAdapter = class {
|
|
|
483
493
|
this.run(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${typeSpec}`);
|
|
484
494
|
}
|
|
485
495
|
}
|
|
496
|
+
// ── Async surface ───────────────────────────────────────────────────
|
|
497
|
+
// SQLite is fundamentally synchronous (better-sqlite3 by design), so the
|
|
498
|
+
// async methods just wrap the sync versions in resolved Promises. They
|
|
499
|
+
// exist so callers can write a single async-preferring code path that
|
|
500
|
+
// works against both backends without branching on dialect.
|
|
501
|
+
async runAsync(sql, params) {
|
|
502
|
+
this.run(sql, params);
|
|
503
|
+
}
|
|
504
|
+
async getAsync(sql, params) {
|
|
505
|
+
return this.get(sql, params);
|
|
506
|
+
}
|
|
507
|
+
async allAsync(sql, params) {
|
|
508
|
+
return this.all(sql, params);
|
|
509
|
+
}
|
|
510
|
+
async introspectColumnsAsync(table) {
|
|
511
|
+
return this.introspectColumns(table);
|
|
512
|
+
}
|
|
513
|
+
async addColumnAsync(table, column, typeSpec) {
|
|
514
|
+
this.addColumn(table, column, typeSpec);
|
|
515
|
+
}
|
|
486
516
|
/**
|
|
487
517
|
* Run `fn` inside a BEGIN/COMMIT block on the single SQLite connection.
|
|
488
518
|
* The TxClient surface delegates to the same underlying database as the
|
|
@@ -547,30 +577,21 @@ function moduleContext() {
|
|
|
547
577
|
}
|
|
548
578
|
return _moduleContext;
|
|
549
579
|
}
|
|
580
|
+
var SYNC_NOT_SUPPORTED_MSG = "PostgresAdapter: synchronous adapter methods (run/get/all/prepare/introspectColumns/addColumn) are no longer supported on Postgres as of latticesql 1.10.0. Use the async surface (runAsync/getAsync/allAsync/prepareAsync/introspectColumnsAsync/addColumnAsync/withClient) instead. Lattice core methods (Lattice.query, .insert, .update, .render, etc.) already route through the async surface \u2014 only consumer code that escapes into adapter.run/get/all directly needs migrating.";
|
|
550
581
|
var PostgresAdapter = class {
|
|
551
582
|
dialect = "postgres";
|
|
552
583
|
_connectionString;
|
|
553
|
-
_workerPath;
|
|
554
584
|
_poolSize;
|
|
555
|
-
_syncFn = null;
|
|
556
585
|
_pool = null;
|
|
586
|
+
_polyfillsReady = null;
|
|
557
587
|
_opened = false;
|
|
558
588
|
constructor(connectionString, options = {}) {
|
|
559
589
|
this._connectionString = connectionString;
|
|
560
|
-
this._workerPath = options.workerPath ?? path.join(moduleContext().dir, "postgres-worker.cjs");
|
|
561
590
|
this._poolSize = options.poolSize ?? 10;
|
|
562
591
|
}
|
|
563
592
|
open() {
|
|
564
593
|
if (this._opened) return;
|
|
565
594
|
const ctxRequire = moduleContext().require;
|
|
566
|
-
let createSyncFn;
|
|
567
|
-
try {
|
|
568
|
-
({ createSyncFn } = ctxRequire("synckit"));
|
|
569
|
-
} catch (err) {
|
|
570
|
-
throw new Error(
|
|
571
|
-
"PostgresAdapter requires 'synckit'. Install with: npm install synckit\nUnderlying error: " + (err instanceof Error ? err.message : String(err))
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
595
|
let pgMod;
|
|
575
596
|
try {
|
|
576
597
|
pgMod = ctxRequire("pg");
|
|
@@ -579,75 +600,62 @@ var PostgresAdapter = class {
|
|
|
579
600
|
"PostgresAdapter requires 'pg'. Install with: npm install pg\nUnderlying error: " + (err instanceof Error ? err.message : String(err))
|
|
580
601
|
);
|
|
581
602
|
}
|
|
582
|
-
this._syncFn = createSyncFn(this._workerPath);
|
|
583
|
-
this._call({ type: "open", connectionString: this._connectionString });
|
|
584
603
|
this._pool = new pgMod.Pool({
|
|
585
604
|
connectionString: this._connectionString,
|
|
586
605
|
max: this._poolSize
|
|
587
606
|
});
|
|
607
|
+
this._polyfillsReady = this._registerPolyfills();
|
|
588
608
|
this._opened = true;
|
|
589
609
|
}
|
|
590
610
|
close() {
|
|
591
611
|
if (!this._opened) return;
|
|
592
|
-
this._call({ type: "close" });
|
|
593
612
|
if (this._pool) {
|
|
594
613
|
void this._pool.end().catch(() => {
|
|
595
614
|
});
|
|
596
615
|
this._pool = null;
|
|
597
616
|
}
|
|
617
|
+
this._polyfillsReady = null;
|
|
598
618
|
this._opened = false;
|
|
599
|
-
this._syncFn = null;
|
|
600
619
|
}
|
|
601
|
-
|
|
602
|
-
|
|
620
|
+
// ── Sync surface (no longer supported on Postgres) ──────────────────
|
|
621
|
+
// The synchronous methods on StorageAdapter exist for SQLite consumers
|
|
622
|
+
// (better-sqlite3 is sync by design). On Postgres they throw — `pg.Pool`
|
|
623
|
+
// is fundamentally async and the synckit-bridged sync path was removed
|
|
624
|
+
// in 1.10.0 to drop the `Atomics.wait` blocking it imposed on the Node
|
|
625
|
+
// main thread.
|
|
626
|
+
run(_sql, _params = []) {
|
|
627
|
+
throw new Error(SYNC_NOT_SUPPORTED_MSG);
|
|
603
628
|
}
|
|
604
|
-
get(
|
|
605
|
-
|
|
606
|
-
return r.rows?.[0];
|
|
629
|
+
get(_sql, _params = []) {
|
|
630
|
+
throw new Error(SYNC_NOT_SUPPORTED_MSG);
|
|
607
631
|
}
|
|
608
|
-
all(
|
|
609
|
-
|
|
610
|
-
return r.rows ?? [];
|
|
632
|
+
all(_sql, _params = []) {
|
|
633
|
+
throw new Error(SYNC_NOT_SUPPORTED_MSG);
|
|
611
634
|
}
|
|
612
|
-
prepare(
|
|
613
|
-
|
|
614
|
-
return {
|
|
615
|
-
run: (...params) => {
|
|
616
|
-
const r = this._call({ type: "run", sql: rewritten, params });
|
|
617
|
-
return { changes: r.rowCount ?? 0, lastInsertRowid: 0 };
|
|
618
|
-
},
|
|
619
|
-
get: (...params) => {
|
|
620
|
-
const r = this._call({ type: "get", sql: rewritten, params });
|
|
621
|
-
return r.rows?.[0];
|
|
622
|
-
},
|
|
623
|
-
all: (...params) => {
|
|
624
|
-
const r = this._call({ type: "all", sql: rewritten, params });
|
|
625
|
-
return r.rows ?? [];
|
|
626
|
-
}
|
|
627
|
-
};
|
|
635
|
+
prepare(_sql) {
|
|
636
|
+
throw new Error(SYNC_NOT_SUPPORTED_MSG);
|
|
628
637
|
}
|
|
629
|
-
introspectColumns(
|
|
630
|
-
|
|
631
|
-
return (r.rows ?? []).map((row) => row.column_name);
|
|
638
|
+
introspectColumns(_table) {
|
|
639
|
+
throw new Error(SYNC_NOT_SUPPORTED_MSG);
|
|
632
640
|
}
|
|
633
|
-
addColumn(
|
|
634
|
-
|
|
641
|
+
addColumn(_table, _column, _typeSpec) {
|
|
642
|
+
throw new Error(SYNC_NOT_SUPPORTED_MSG);
|
|
635
643
|
}
|
|
636
644
|
// ── Async surface ───────────────────────────────────────────────────
|
|
637
|
-
// Native against pg.Pool.
|
|
638
|
-
//
|
|
639
|
-
//
|
|
645
|
+
// Native against pg.Pool. The Node event loop is free to handle other
|
|
646
|
+
// work (HTTP requests, Slack socket pings, scheduler timers, etc.) while
|
|
647
|
+
// these calls await DB I/O.
|
|
640
648
|
async runAsync(sql, params = []) {
|
|
641
|
-
const pool = this.
|
|
649
|
+
const pool = await this._readyPool();
|
|
642
650
|
await pool.query(rewrite(sql), params);
|
|
643
651
|
}
|
|
644
652
|
async getAsync(sql, params = []) {
|
|
645
|
-
const pool = this.
|
|
653
|
+
const pool = await this._readyPool();
|
|
646
654
|
const r = await pool.query(rewrite(sql), params);
|
|
647
655
|
return r.rows[0];
|
|
648
656
|
}
|
|
649
657
|
async allAsync(sql, params = []) {
|
|
650
|
-
const pool = this.
|
|
658
|
+
const pool = await this._readyPool();
|
|
651
659
|
const r = await pool.query(rewrite(sql), params);
|
|
652
660
|
return r.rows;
|
|
653
661
|
}
|
|
@@ -669,22 +677,41 @@ var PostgresAdapter = class {
|
|
|
669
677
|
const rewritten = rewrite(sql);
|
|
670
678
|
return {
|
|
671
679
|
run: async (...params) => {
|
|
672
|
-
const pool = this.
|
|
680
|
+
const pool = await this._readyPool();
|
|
673
681
|
const r = await pool.query(rewritten, params);
|
|
674
682
|
return { changes: r.rowCount ?? 0, lastInsertRowid: 0 };
|
|
675
683
|
},
|
|
676
684
|
get: async (...params) => {
|
|
677
|
-
const pool = this.
|
|
685
|
+
const pool = await this._readyPool();
|
|
678
686
|
const r = await pool.query(rewritten, params);
|
|
679
687
|
return r.rows[0];
|
|
680
688
|
},
|
|
681
689
|
all: async (...params) => {
|
|
682
|
-
const pool = this.
|
|
690
|
+
const pool = await this._readyPool();
|
|
683
691
|
const r = await pool.query(rewritten, params);
|
|
684
692
|
return r.rows;
|
|
685
693
|
}
|
|
686
694
|
};
|
|
687
695
|
}
|
|
696
|
+
async introspectColumnsAsync(table) {
|
|
697
|
+
const pool = await this._readyPool();
|
|
698
|
+
const r = await pool.query(
|
|
699
|
+
`SELECT column_name FROM information_schema.columns
|
|
700
|
+
WHERE table_schema = current_schema() AND table_name = $1
|
|
701
|
+
ORDER BY ordinal_position`,
|
|
702
|
+
[table]
|
|
703
|
+
);
|
|
704
|
+
return r.rows.map((row) => row.column_name);
|
|
705
|
+
}
|
|
706
|
+
async addColumnAsync(table, column, typeSpec) {
|
|
707
|
+
const upper = typeSpec.toUpperCase();
|
|
708
|
+
if (upper.includes("PRIMARY KEY")) return;
|
|
709
|
+
const translated = translateTypeSpec(typeSpec);
|
|
710
|
+
const pool = await this._readyPool();
|
|
711
|
+
await pool.query(
|
|
712
|
+
`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" ${translated}`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
688
715
|
/**
|
|
689
716
|
* Run `fn` against a single checked-out pool client wrapped in BEGIN/COMMIT.
|
|
690
717
|
* The TxClient handed to `fn` runs every query against the same upstream
|
|
@@ -697,7 +724,7 @@ var PostgresAdapter = class {
|
|
|
697
724
|
* the connection rather than recycling a known-bad one.
|
|
698
725
|
*/
|
|
699
726
|
async withClient(fn) {
|
|
700
|
-
const pool = this.
|
|
727
|
+
const pool = await this._readyPool();
|
|
701
728
|
const client = await pool.connect();
|
|
702
729
|
const tx = {
|
|
703
730
|
run: async (sql, params) => {
|
|
@@ -732,19 +759,96 @@ var PostgresAdapter = class {
|
|
|
732
759
|
client.release(releaseErr);
|
|
733
760
|
}
|
|
734
761
|
}
|
|
735
|
-
|
|
762
|
+
/**
|
|
763
|
+
* Resolve the pool, waiting for one-time polyfill registration to complete.
|
|
764
|
+
* Every async method funnels through here so the polyfills are guaranteed
|
|
765
|
+
* to be in place by the time the caller's first query runs.
|
|
766
|
+
*/
|
|
767
|
+
async _readyPool() {
|
|
736
768
|
if (!this._pool) {
|
|
737
769
|
throw new Error("PostgresAdapter: not open \u2014 call open() first");
|
|
738
770
|
}
|
|
771
|
+
if (this._polyfillsReady) await this._polyfillsReady;
|
|
739
772
|
return this._pool;
|
|
740
773
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
774
|
+
/**
|
|
775
|
+
* Idempotently register the SQLite-compat polyfills the dialect translator
|
|
776
|
+
* relies on:
|
|
777
|
+
* - `pgcrypto` extension — provides `gen_random_bytes()` for the
|
|
778
|
+
* `randomblob()` translation.
|
|
779
|
+
* - `json_extract(doc, path)` SQL function — mimics SQLite's
|
|
780
|
+
* `$.a.b.c` path syntax against jsonb.
|
|
781
|
+
* - `strftime(format, modifier)` SQL function — handles the common
|
|
782
|
+
* `strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` pattern lattice itself emits
|
|
783
|
+
* for ISO timestamps.
|
|
784
|
+
*
|
|
785
|
+
* Each registration is wrapped in try/catch so a permission-restricted
|
|
786
|
+
* provider (e.g. some managed Postgres tiers don't allow CREATE EXTENSION)
|
|
787
|
+
* surfaces a non-fatal warning rather than blocking pool readiness.
|
|
788
|
+
*/
|
|
789
|
+
async _registerPolyfills() {
|
|
790
|
+
if (!this._pool) return;
|
|
791
|
+
const pool = this._pool;
|
|
792
|
+
try {
|
|
793
|
+
await pool.query("CREATE EXTENSION IF NOT EXISTS pgcrypto");
|
|
794
|
+
} catch (extErr) {
|
|
795
|
+
console.warn(
|
|
796
|
+
"[PostgresAdapter] CREATE EXTENSION pgcrypto failed (may already be enabled by your provider):",
|
|
797
|
+
extErr instanceof Error ? extErr.message : extErr
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
await pool.query(
|
|
802
|
+
`CREATE OR REPLACE FUNCTION json_extract(doc text, path text)
|
|
803
|
+
RETURNS text
|
|
804
|
+
LANGUAGE sql
|
|
805
|
+
IMMUTABLE
|
|
806
|
+
AS $fn$
|
|
807
|
+
SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
|
|
808
|
+
$fn$;`
|
|
809
|
+
);
|
|
810
|
+
} catch (jeErr) {
|
|
811
|
+
console.warn(
|
|
812
|
+
"[PostgresAdapter] could not register json_extract polyfill:",
|
|
813
|
+
jeErr instanceof Error ? jeErr.message : jeErr
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
await pool.query(
|
|
818
|
+
`CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
|
|
819
|
+
RETURNS text
|
|
820
|
+
LANGUAGE plpgsql
|
|
821
|
+
IMMUTABLE
|
|
822
|
+
AS $fn$
|
|
823
|
+
DECLARE ts timestamptz;
|
|
824
|
+
BEGIN
|
|
825
|
+
IF modifier = 'now' THEN
|
|
826
|
+
ts := now();
|
|
827
|
+
ELSE
|
|
828
|
+
ts := modifier::timestamptz;
|
|
829
|
+
END IF;
|
|
830
|
+
RETURN to_char(
|
|
831
|
+
ts AT TIME ZONE 'UTC',
|
|
832
|
+
replace(replace(replace(replace(replace(replace(replace(replace(
|
|
833
|
+
format,
|
|
834
|
+
'%Y', 'YYYY'),
|
|
835
|
+
'%m', 'MM'),
|
|
836
|
+
'%d', 'DD'),
|
|
837
|
+
'%H', 'HH24'),
|
|
838
|
+
'%M', 'MI'),
|
|
839
|
+
'%S', 'SS'),
|
|
840
|
+
'%f', 'MS'),
|
|
841
|
+
'T', '"T"')
|
|
842
|
+
);
|
|
843
|
+
END;
|
|
844
|
+
$fn$;`
|
|
845
|
+
);
|
|
846
|
+
} catch (sfErr) {
|
|
847
|
+
console.warn(
|
|
848
|
+
"[PostgresAdapter] could not register strftime polyfill:",
|
|
849
|
+
sfErr instanceof Error ? sfErr.message : sfErr
|
|
850
|
+
);
|
|
746
851
|
}
|
|
747
|
-
return result;
|
|
748
852
|
}
|
|
749
853
|
};
|
|
750
854
|
function translateDialect(sql) {
|
|
@@ -780,6 +884,9 @@ function translateDialect(sql) {
|
|
|
780
884
|
});
|
|
781
885
|
return s;
|
|
782
886
|
}
|
|
887
|
+
function translateTypeSpec(typeSpec) {
|
|
888
|
+
return typeSpec.replace(/\bBLOB\b/gi, "BYTEA").replace(/\bdatetime\(\s*'now'\s*\)/gi, "NOW()").replace(/\bRANDOM\(\)/gi, "random()");
|
|
889
|
+
}
|
|
783
890
|
function hasOnConflictInCode(sql) {
|
|
784
891
|
let found = false;
|
|
785
892
|
mapCodeRegions(sql, (code) => {
|
|
@@ -1166,7 +1273,7 @@ var SchemaManager = class {
|
|
|
1166
1273
|
return allAsyncOrSync(adapter, `SELECT * FROM "${name}"`);
|
|
1167
1274
|
}
|
|
1168
1275
|
if (this._entityContexts.has(name)) {
|
|
1169
|
-
const cols = adapter
|
|
1276
|
+
const cols = await introspectColumnsAsyncOrSync(adapter, name);
|
|
1170
1277
|
const hasDeletedAt = cols.includes("deleted_at");
|
|
1171
1278
|
return allAsyncOrSync(
|
|
1172
1279
|
adapter,
|
|
@@ -1182,13 +1289,13 @@ var SchemaManager = class {
|
|
|
1182
1289
|
adapter,
|
|
1183
1290
|
`CREATE TABLE IF NOT EXISTS "${name}" (${colDefs}${constraintDefs})`
|
|
1184
1291
|
);
|
|
1185
|
-
this._addMissingColumns(adapter, name, columns);
|
|
1292
|
+
await this._addMissingColumns(adapter, name, columns);
|
|
1186
1293
|
}
|
|
1187
|
-
_addMissingColumns(adapter, table, columns) {
|
|
1188
|
-
const existing = adapter
|
|
1294
|
+
async _addMissingColumns(adapter, table, columns) {
|
|
1295
|
+
const existing = await introspectColumnsAsyncOrSync(adapter, table);
|
|
1189
1296
|
for (const [col, type] of Object.entries(columns)) {
|
|
1190
1297
|
if (existing.includes(col)) continue;
|
|
1191
|
-
adapter
|
|
1298
|
+
await addColumnAsyncOrSync(adapter, table, col, type);
|
|
1192
1299
|
}
|
|
1193
1300
|
}
|
|
1194
1301
|
};
|
|
@@ -2472,7 +2579,7 @@ var ReverseSeedEngine = class {
|
|
|
2472
2579
|
* and the "did the insert actually add a row" check would be racy.
|
|
2473
2580
|
*/
|
|
2474
2581
|
async _insertOrIgnore(tx, table, row) {
|
|
2475
|
-
const validColumns = new Set(this._adapter
|
|
2582
|
+
const validColumns = new Set(await introspectColumnsAsyncOrSync(this._adapter, table));
|
|
2476
2583
|
const filtered = {};
|
|
2477
2584
|
for (const [key, val] of Object.entries(row)) {
|
|
2478
2585
|
if (validColumns.has(key)) {
|
|
@@ -3033,9 +3140,10 @@ var Lattice = class {
|
|
|
3033
3140
|
await this._schema.applyMigrationsAsync(this._adapter, options.migrations);
|
|
3034
3141
|
}
|
|
3035
3142
|
for (const tableName of this._schema.getTables().keys()) {
|
|
3036
|
-
|
|
3143
|
+
const cols = await introspectColumnsAsyncOrSync(this._adapter, tableName);
|
|
3144
|
+
this._columnCache.set(tableName, new Set(cols));
|
|
3037
3145
|
}
|
|
3038
|
-
this._finalizeEncryptionSetup();
|
|
3146
|
+
await this._finalizeEncryptionSetup();
|
|
3039
3147
|
const hasEmbeddings = [...this._schema.getTables().values()].some((d) => d.embeddings);
|
|
3040
3148
|
if (hasEmbeddings) {
|
|
3041
3149
|
await ensureEmbeddingsTable(this._adapter);
|
|
@@ -3058,7 +3166,8 @@ var Lattice = class {
|
|
|
3058
3166
|
}
|
|
3059
3167
|
await this._schema.applyMigrationsAsync(this._adapter, migrations);
|
|
3060
3168
|
for (const tableName of this._schema.getTables().keys()) {
|
|
3061
|
-
|
|
3169
|
+
const cols = await introspectColumnsAsyncOrSync(this._adapter, tableName);
|
|
3170
|
+
this._columnCache.set(tableName, new Set(cols));
|
|
3062
3171
|
}
|
|
3063
3172
|
}
|
|
3064
3173
|
close() {
|
|
@@ -3109,12 +3218,12 @@ var Lattice = class {
|
|
|
3109
3218
|
* see the post-migration schema. Runs in the async tail of init() after
|
|
3110
3219
|
* applySchema/applyMigrationsAsync.
|
|
3111
3220
|
*/
|
|
3112
|
-
_finalizeEncryptionSetup() {
|
|
3221
|
+
async _finalizeEncryptionSetup() {
|
|
3113
3222
|
for (const [table, def] of this._schema.getEntityContexts()) {
|
|
3114
3223
|
if (!def.encrypted) continue;
|
|
3115
3224
|
if (!this._encryptionKeyRaw) continue;
|
|
3116
3225
|
this._encryptionKey ??= deriveKey(this._encryptionKeyRaw);
|
|
3117
|
-
const allCols = this._adapter
|
|
3226
|
+
const allCols = await introspectColumnsAsyncOrSync(this._adapter, table);
|
|
3118
3227
|
const encCols = resolveEncryptedColumns(def.encrypted, allCols);
|
|
3119
3228
|
this._encryptedTableColumns.set(table, encCols);
|
|
3120
3229
|
}
|
|
@@ -3977,14 +4086,25 @@ var Lattice = class {
|
|
|
3977
4086
|
* objects are interpolated into SQL, so stripping unknown keys eliminates
|
|
3978
4087
|
* any theoretical injection vector from crafted object keys.
|
|
3979
4088
|
*/
|
|
3980
|
-
/**
|
|
4089
|
+
/**
|
|
4090
|
+
* Return the column cache for a registered table. The cache is pre-populated
|
|
4091
|
+
* for every `define()`d table at the end of `_initAsync` (after migrations
|
|
4092
|
+
* apply, so migration-added columns are visible). Tables accessed through
|
|
4093
|
+
* the raw `.db` / `.adapter` escape hatch — outside lattice's `define()`
|
|
4094
|
+
* contract — return an empty set; their callers' `_filterToSchemaColumns`
|
|
4095
|
+
* short-circuit ("unknown table — pass through") is the right behavior for
|
|
4096
|
+
* those, since column filtering needs a known column list.
|
|
4097
|
+
*
|
|
4098
|
+
* Pre-1.10.0 this method had a lazy `introspectColumns` fallback for
|
|
4099
|
+
* unregistered tables. The fallback was dropped when synckit was removed —
|
|
4100
|
+
* synchronous Postgres introspection has no path on `pg.Pool`. The
|
|
4101
|
+
* effective behavior change is: raw-.db writes to a table that lattice
|
|
4102
|
+
* never `define()`d no longer get their `Row` filtered to "schema-known
|
|
4103
|
+
* columns". That contract is preserved for every `define()`d table, which
|
|
4104
|
+
* is what production code uses.
|
|
4105
|
+
*/
|
|
3981
4106
|
_ensureColumnCache(table) {
|
|
3982
|
-
|
|
3983
|
-
if (!cols) {
|
|
3984
|
-
cols = new Set(this._adapter.introspectColumns(table));
|
|
3985
|
-
if (cols.size > 0) this._columnCache.set(table, cols);
|
|
3986
|
-
}
|
|
3987
|
-
return cols;
|
|
4107
|
+
return this._columnCache.get(table) ?? /* @__PURE__ */ new Set();
|
|
3988
4108
|
}
|
|
3989
4109
|
_filterToSchemaColumns(table, row) {
|
|
3990
4110
|
const cols = this._ensureColumnCache(table);
|