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 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 dependencies:
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 synckit
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` and `synckit`. Both are listed as `optionalDependencies`, so SQLite-only consumers don't pay the install cost. Install them when you actually use Postgres:
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 synckit
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
- **Connection pooler note:** the `PostgresAdapter` exposes both a synckit-bridged sync surface and a native `pg.Pool`-backed async surface (since 1.8.0). Pick your pooler endpoint based on which surface you primarily use:
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
- **Two surfaces, one adapter (since 1.8.0):**
2173
+ **Async-only on Postgres (since 1.10.0):**
2175
2174
 
2176
- - **Sync surface** (`run` / `get` / `all` / `prepare`) bridged via a synckit worker thread that owns a single `pg.Client`. The main thread blocks on `Atomics.wait` until the worker replies. Each query pays ~1–3 ms of message-passing overhead. Preserved for back-compat with third-party adapters that don't implement the async surface; lattice itself no longer uses it on the hot path.
2177
- - **Async surface** (`runAsync` / `getAsync` / `allAsync` / `prepareAsync` / `withClient`) — native against a `pg.Pool` in the main thread. No synckit, no `Atomics.wait`. The Node event loop is free to handle other work between awaited DB roundtrips. **Since 1.9.0, lattice core internally prefers this surface at every call site** when the configured adapter implements it (Postgres)fall-back to the sync surface only when the adapter doesn't expose async methods (SQLite). `pool.max` is configurable via `PostgresAdapterOptions.poolSize` (default 10).
2178
- - **Transactional contract**: any code that issues `BEGIN`/`COMMIT` should use `withClient(fn)` rather than raw `adapter.run('BEGIN')`. 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.run('BEGIN')` is only safe under the synckit worker (single-connection by construction); the next major release will remove the synckit worker entirely.
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
- run(sql, params = []) {
602
- this._call({ type: "run", sql: rewrite(sql), params });
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(sql, params = []) {
605
- const r = this._call({ type: "get", sql: rewrite(sql), params });
606
- return r.rows?.[0];
629
+ get(_sql, _params = []) {
630
+ throw new Error(SYNC_NOT_SUPPORTED_MSG);
607
631
  }
608
- all(sql, params = []) {
609
- const r = this._call({ type: "all", sql: rewrite(sql), params });
610
- return r.rows ?? [];
632
+ all(_sql, _params = []) {
633
+ throw new Error(SYNC_NOT_SUPPORTED_MSG);
611
634
  }
612
- prepare(sql) {
613
- const rewritten = rewrite(sql);
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(table) {
630
- const r = this._call({ type: "introspectColumns", table });
631
- return (r.rows ?? []).map((row) => row.column_name);
638
+ introspectColumns(_table) {
639
+ throw new Error(SYNC_NOT_SUPPORTED_MSG);
632
640
  }
633
- addColumn(table, column, typeSpec) {
634
- this._call({ type: "addColumn", table, column, typeSpec });
641
+ addColumn(_table, _column, _typeSpec) {
642
+ throw new Error(SYNC_NOT_SUPPORTED_MSG);
635
643
  }
636
644
  // ── Async surface ───────────────────────────────────────────────────
637
- // Native against pg.Pool. No synckit, no Atomics.wait. The Node event
638
- // loop is free to handle other work (HTTP requests, Slack socket pings,
639
- // scheduler timers, etc.) while these calls await DB I/O.
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._requirePool();
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._requirePool();
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._requirePool();
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._requirePool();
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._requirePool();
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._requirePool();
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._requirePool();
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
- _requirePool() {
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
- _call(action) {
742
- if (!this._syncFn) throw new Error("PostgresAdapter: not open \u2014 call open() first");
743
- const result = this._syncFn(action);
744
- if (!result.ok) {
745
- throw new Error(`PostgresAdapter: ${result.error}`);
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.introspectColumns(name);
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.introspectColumns(table);
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.addColumn(table, col, type);
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.introspectColumns(table));
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
- this._columnCache.set(tableName, new Set(this._adapter.introspectColumns(tableName)));
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
- this._columnCache.set(tableName, new Set(this._adapter.introspectColumns(tableName)));
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.introspectColumns(table);
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
- /** Lazily populate column cache for tables not registered via define(). */
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
- let cols = this._columnCache.get(table);
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);