prisma-pglite-bridge 1.1.0 → 1.2.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/dist/index.cjs CHANGED
@@ -183,6 +183,7 @@ const rewriteRowDescriptionInPlace = (buf) => {
183
183
  */
184
184
  var BackendMessageFramer = class {
185
185
  suppressIntermediateReadyForQuery;
186
+ rewriteSystemCatalogCharOids;
186
187
  onChunk;
187
188
  onErrorResponse;
188
189
  onReadyForQuery;
@@ -198,6 +199,7 @@ var BackendMessageFramer = class {
198
199
  rowDescOffset = 0;
199
200
  constructor(options) {
200
201
  this.suppressIntermediateReadyForQuery = options.suppressIntermediateReadyForQuery ?? false;
202
+ this.rewriteSystemCatalogCharOids = options.rewriteSystemCatalogCharOids ?? true;
201
203
  this.onChunk = options.onChunk;
202
204
  this.onErrorResponse = options.onErrorResponse;
203
205
  this.onReadyForQuery = options.onReadyForQuery;
@@ -231,7 +233,7 @@ var BackendMessageFramer = class {
231
233
  if (msgType === 69) this.onErrorResponse?.();
232
234
  if (msgType === 90 && messageLength === 5) {
233
235
  flushPassthrough(offset);
234
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
236
+ this.dropStaleHeldReadyForQuery();
235
237
  /* c8 ignore next — messageLength === 5 for RFQ; payload is 1 byte */
236
238
  const status = chunk[offset + 5] ?? 0;
237
239
  this.heldRfq[0] = msgType;
@@ -246,16 +248,12 @@ var BackendMessageFramer = class {
246
248
  this.emitReadyForQuery();
247
249
  this.rfqBytesRead = 0;
248
250
  }
249
- } else if (msgType === 84) if (rowDescriptionNeedsRewrite(chunk, offset, offset + totalLen)) {
251
+ } else if (msgType === 84 && this.rewriteSystemCatalogCharOids && rowDescriptionNeedsRewrite(chunk, offset, offset + totalLen)) {
250
252
  flushPassthrough(offset);
251
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
253
+ this.dropStaleHeldReadyForQuery();
252
254
  this.emitRewrittenRowDescription(Buffer.from(chunk.subarray(offset, offset + totalLen)));
253
255
  } else {
254
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
255
- if (passthroughStart < 0) passthroughStart = offset;
256
- }
257
- else {
258
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
256
+ this.dropStaleHeldReadyForQuery();
259
257
  if (passthroughStart < 0) passthroughStart = offset;
260
258
  }
261
259
  offset += totalLen;
@@ -263,7 +261,7 @@ var BackendMessageFramer = class {
263
261
  }
264
262
  }
265
263
  flushPassthrough(offset);
266
- if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
264
+ this.dropStaleHeldReadyForQuery();
267
265
  /* c8 ignore next — offset < chunk.length guaranteed by outer while */
268
266
  this.messageType = chunk[offset] ?? 0;
269
267
  this.headerBytesRead = 0;
@@ -297,7 +295,7 @@ var BackendMessageFramer = class {
297
295
  if (this.messageType === 69) this.onErrorResponse?.();
298
296
  if (this.isReadyForQueryFrame()) continue;
299
297
  this.dropHeldReadyForQuery();
300
- if (this.messageType === 84) {
298
+ if (this.messageType === 84 && this.rewriteSystemCatalogCharOids) {
301
299
  this.rowDescBuffer = Buffer.alloc(5 + this.payloadBytesRemaining);
302
300
  this.rowDescBuffer[0] = 84;
303
301
  this.rowDescBuffer.set(this.headerScratch, 1);
@@ -347,14 +345,6 @@ var BackendMessageFramer = class {
347
345
  this.rfqBytesRead = 0;
348
346
  }
349
347
  }
350
- reset() {
351
- this.messageType = void 0;
352
- this.headerBytesRead = 0;
353
- this.payloadBytesRemaining = 0;
354
- this.rfqBytesRead = 0;
355
- this.rowDescBuffer = void 0;
356
- this.rowDescOffset = 0;
357
- }
358
348
  isReadyForQueryFrame() {
359
349
  return this.messageType === 90 && this.payloadBytesRemaining === 1;
360
350
  }
@@ -371,6 +361,11 @@ var BackendMessageFramer = class {
371
361
  dropHeldReadyForQuery() {
372
362
  this.rfqBytesRead = 0;
373
363
  }
364
+ /** Drop a complete held RFQ once a following frame proves it was an
365
+ * intermediate one. No-op unless suppressing and a full RFQ is buffered. */
366
+ dropStaleHeldReadyForQuery() {
367
+ if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
368
+ }
374
369
  emitPrefix() {
375
370
  const prefix = new Uint8Array(5);
376
371
  /* c8 ignore next — messageType always set when emitPrefix is called */
@@ -575,6 +570,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
575
570
  bridgeId;
576
571
  telemetry;
577
572
  syncToFs;
573
+ rewriteSystemCatalogCharOids;
578
574
  timeout;
579
575
  duplexId;
580
576
  /** Incoming bytes framed directly from a queued chunk buffer */
@@ -617,6 +613,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
617
613
  this.telemetry = options.telemetry;
618
614
  this.timeout = options.timeout;
619
615
  this.syncToFs = options.syncToFs ?? true;
616
+ this.rewriteSystemCatalogCharOids = options.rewriteSystemCatalogCharOids ?? true;
620
617
  this.duplexId = Symbol("duplex");
621
618
  this.onClose = new Promise((resolve) => this.once("close", () => resolve()));
622
619
  }
@@ -722,9 +719,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
722
719
  /* c8 ignore next — len === undefined unreachable once length ≥ 4 */
723
720
  if (len === void 0 || this.input.length < len) return;
724
721
  const message = this.input.consume(len);
725
- const session = this.acquireSession();
726
- if (session) await session;
727
- await this.runUnderRunExclusive(async () => {
722
+ await this.runUntimed(async () => {
728
723
  await this.streamProtocol(message, {
729
724
  detectErrors: false,
730
725
  suppressIntermediateRfq: false
@@ -758,6 +753,13 @@ var PGliteDuplex = class extends node_stream.Duplex {
758
753
  }
759
754
  });
760
755
  }
756
+ /** Acquire the session, then run `op` under runExclusive without timing —
757
+ * the untimed path shared by startup and the no-telemetry query branch. */
758
+ async runUntimed(op) {
759
+ const session = this.acquireSession();
760
+ if (session) await session;
761
+ await this.runUnderRunExclusive(op);
762
+ }
761
763
  /**
762
764
  * Frames and processes regular wire protocol messages.
763
765
  *
@@ -866,9 +868,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
866
868
  const wantTiming = wantTelemetry || publishQuery || publishLockWait;
867
869
  const detectErrors = wantTelemetry || publishQuery;
868
870
  if (!wantTiming) {
869
- const session = this.acquireSession();
870
- if (session) await session;
871
- await this.runUnderRunExclusive(async () => {
871
+ await this.runUntimed(async () => {
872
872
  await op(false);
873
873
  });
874
874
  return;
@@ -915,6 +915,7 @@ var PGliteDuplex = class extends node_stream.Duplex {
915
915
  let errSeen = false;
916
916
  const framer = new BackendMessageFramer({
917
917
  suppressIntermediateReadyForQuery: suppressIntermediateRfq,
918
+ rewriteSystemCatalogCharOids: this.rewriteSystemCatalogCharOids,
918
919
  onChunk: (chunk) => {
919
920
  /* c8 ignore next — race-only: tornDown becomes true mid-stream */
920
921
  if (!this.tornDown && chunk.length > 0) this.push(chunk);
@@ -1119,17 +1120,12 @@ var SessionLock = class {
1119
1120
  this.waitQueue = remaining;
1120
1121
  return cancelled;
1121
1122
  }
1122
- /**
1123
- * Grant ownership to the next waiter, if any.
1124
- *
1125
- * @returns `true` if a waiter was unblocked; `false` if the queue was empty.
1126
- */
1123
+ /** Grant ownership to the next waiter, if any. */
1127
1124
  drainWaitQueue() {
1128
1125
  const next = this.waitQueue.shift();
1129
- if (!next) return false;
1126
+ if (!next) return;
1130
1127
  this.owner = next.id;
1131
1128
  next.resolve();
1132
- return true;
1133
1129
  }
1134
1130
  };
1135
1131
  //#endregion
@@ -1348,16 +1344,7 @@ var PgBridgePool = class extends pg.default.Pool {
1348
1344
  return super.end().then(cleanup);
1349
1345
  }
1350
1346
  };
1351
- //#endregion
1352
- //#region src/telemetry/bridge-stats.ts
1353
- /**
1354
- * Maximum number of recent query durations retained for percentile
1355
- * computation. Beyond this window, `recentP50QueryMs`, `recentP95QueryMs`,
1356
- * and `recentMaxQueryMs` reflect only the most recent N queries — lifetime
1357
- * counters (`queryCount`, `totalQueryMs`, `avgQueryMs`) remain complete.
1358
- */
1359
- const QUERY_DURATION_WINDOW_SIZE = 1e4;
1360
- const QUERY_DURATION_TRIM_THRESHOLD = QUERY_DURATION_WINDOW_SIZE * 2;
1347
+ const QUERY_DURATION_TRIM_THRESHOLD = 1e4 * 2;
1361
1348
  const DB_SIZE_QUERY_TIMEOUT_MS = 5e3;
1362
1349
  /**
1363
1350
  * `process.resourceUsage().maxRSS` returns kilobytes on every platform
@@ -1402,7 +1389,7 @@ var BridgeStats = class {
1402
1389
  this.queryCount += 1;
1403
1390
  this.totalQueryMs += durationMs;
1404
1391
  this.queryDurations.push(durationMs);
1405
- if (this.queryDurations.length > QUERY_DURATION_TRIM_THRESHOLD) this.queryDurations = this.queryDurations.slice(-QUERY_DURATION_WINDOW_SIZE);
1392
+ if (this.queryDurations.length > QUERY_DURATION_TRIM_THRESHOLD) this.queryDurations = this.queryDurations.slice(-1e4);
1406
1393
  if (!succeeded) this.failedQueryCount += 1;
1407
1394
  }
1408
1395
  recordLockWait(durationMs) {
@@ -1478,15 +1465,18 @@ var BridgeStats = class {
1478
1465
  }
1479
1466
  };
1480
1467
  //#endregion
1468
+ //#region src/utils/quote-ident.ts
1469
+ /** JS equivalent of PostgreSQL's `quote_ident()`; matches its escaping rules. */
1470
+ const quoteIdent = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1471
+ //#endregion
1481
1472
  //#region src/pglite-bridge/snapshot-manager.ts
1482
1473
  const SNAPSHOT_SCHEMA = "_pglite_snapshot";
1483
- const USER_TABLES_WHERE = `schemaname NOT IN ('pg_catalog', 'information_schema')
1484
- AND schemaname != '${SNAPSHOT_SCHEMA}'
1474
+ const SYSTEM_SCHEMA_EXCLUSION = `schemaname NOT IN ('pg_catalog', 'information_schema')
1475
+ AND schemaname != '${SNAPSHOT_SCHEMA}'`;
1476
+ const USER_TABLES_WHERE = `${SYSTEM_SCHEMA_EXCLUSION}
1485
1477
  AND tablename NOT LIKE '_prisma%'`;
1486
1478
  const escapeLiteral = (s) => `'${s.replace(/'/g, "''")}'`;
1487
- /** JS equivalent of PostgreSQL's `quote_ident()`; matches its escaping rules. */
1488
- const quoteIdent$1 = (identifier) => `"${identifier.replace(/"/g, "\"\"")}"`;
1489
- const SNAPSHOT_SCHEMA_IDENT = quoteIdent$1(SNAPSHOT_SCHEMA);
1479
+ const SNAPSHOT_SCHEMA_IDENT = quoteIdent(SNAPSHOT_SCHEMA);
1490
1480
  const SNAPSHOT_SCHEMA_LITERAL = escapeLiteral(SNAPSHOT_SCHEMA);
1491
1481
  /**
1492
1482
  * Snapshot helpers backing `PGliteBridge`'s `snapshotDb` / `resetDb` /
@@ -1507,32 +1497,30 @@ var SnapshotManager = class {
1507
1497
  * the `_pglite_snapshot` schema. Replaces any previous snapshot.
1508
1498
  */
1509
1499
  async snapshotDb() {
1510
- const pglite = this.#pglite;
1511
- await pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1500
+ await this.#pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1512
1501
  try {
1513
- await pglite.exec("BEGIN");
1514
- await pglite.exec(`CREATE SCHEMA ${SNAPSHOT_SCHEMA_IDENT}`);
1515
- const { rows: tables } = await pglite.query(`SELECT schemaname, tablename,
1502
+ await this.#pglite.exec("BEGIN");
1503
+ await this.#pglite.exec(`CREATE SCHEMA ${SNAPSHOT_SCHEMA_IDENT}`);
1504
+ const { rows: tables } = await this.#pglite.query(`SELECT schemaname, tablename,
1516
1505
  quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
1517
1506
  FROM pg_tables
1518
1507
  WHERE ${USER_TABLES_WHERE}`);
1519
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__tables (snap_name text, source_schema text, source_table text)`);
1508
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__tables (snap_name text, source_schema text, source_table text)`);
1520
1509
  for (const [i, { schemaname, tablename, qualified }] of tables.entries()) {
1521
1510
  const snapName = `_snap_${i}`;
1522
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.${quoteIdent$1(snapName)} AS SELECT * FROM ${qualified}`);
1523
- await pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
1511
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.${quoteIdent(snapName)} AS SELECT * FROM ${qualified}`);
1512
+ await this.#pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__tables VALUES (${escapeLiteral(snapName)}, ${escapeLiteral(schemaname)}, ${escapeLiteral(tablename)})`);
1524
1513
  }
1525
- const { rows: seqs } = await pglite.query(`SELECT quote_literal(quote_ident(schemaname) || '.' || quote_ident(sequencename)) AS name, last_value::text AS value
1514
+ const { rows: seqs } = await this.#pglite.query(`SELECT quote_literal(quote_ident(schemaname) || '.' || quote_ident(sequencename)) AS name, last_value::text AS value
1526
1515
  FROM pg_sequences
1527
- WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
1528
- AND schemaname != '${SNAPSHOT_SCHEMA}'
1516
+ WHERE ${SYSTEM_SCHEMA_EXCLUSION}
1529
1517
  AND last_value IS NOT NULL`);
1530
- await pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__sequences (name text, value bigint)`);
1531
- for (const { name, value } of seqs) await pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__sequences VALUES (${name}, ${value})`);
1532
- await pglite.exec("COMMIT");
1518
+ await this.#pglite.exec(`CREATE TABLE ${SNAPSHOT_SCHEMA_IDENT}.__sequences (name text, value bigint)`);
1519
+ for (const { name, value } of seqs) await this.#pglite.exec(`INSERT INTO ${SNAPSHOT_SCHEMA_IDENT}.__sequences VALUES (${name}, ${value})`);
1520
+ await this.#pglite.exec("COMMIT");
1533
1521
  } catch (err) {
1534
- await pglite.exec("ROLLBACK");
1535
- await pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1522
+ await this.#pglite.exec("ROLLBACK");
1523
+ await this.#pglite.exec(`DROP SCHEMA IF EXISTS ${SNAPSHOT_SCHEMA_IDENT} CASCADE`);
1536
1524
  throw err;
1537
1525
  }
1538
1526
  this.#hasSnapshot = true;
@@ -1547,26 +1535,25 @@ var SnapshotManager = class {
1547
1535
  * sequence values afterwards; otherwise just truncate and `DISCARD ALL`.
1548
1536
  */
1549
1537
  async resetDb() {
1550
- const pglite = this.#pglite;
1551
1538
  if (this.#hasSnapshot) await this.#snapshotSchemaExists();
1552
1539
  const tables = await this.#getTables();
1553
1540
  if (tables) await this.#withReplicationRoleReplica(async () => {
1554
- await pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
1541
+ await this.#pglite.exec(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
1555
1542
  if (!this.#hasSnapshot) return;
1556
- const { rows: snapshotTables } = await pglite.query(`SELECT quote_ident(snap_name) AS snap_name_ident,
1543
+ const { rows: snapshotTables } = await this.#pglite.query(`SELECT quote_ident(snap_name) AS snap_name_ident,
1557
1544
  quote_ident(source_schema) || '.' || quote_ident(source_table) AS qualified
1558
1545
  FROM ${SNAPSHOT_SCHEMA_IDENT}.__tables`);
1559
- for (const { snap_name_ident, qualified } of snapshotTables) await pglite.exec(`INSERT INTO ${qualified} SELECT * FROM ${SNAPSHOT_SCHEMA_IDENT}.${snap_name_ident}`);
1560
- const { rows: seqs } = await pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM ${SNAPSHOT_SCHEMA_IDENT}.__sequences`);
1561
- for (const { name, value } of seqs) await pglite.exec(`SELECT setval(${name}, ${value})`);
1546
+ for (const { snap_name_ident, qualified } of snapshotTables) await this.#pglite.exec(`INSERT INTO ${qualified} SELECT * FROM ${SNAPSHOT_SCHEMA_IDENT}.${snap_name_ident}`);
1547
+ const { rows: seqs } = await this.#pglite.query(`SELECT quote_literal(name) AS name, value::text AS value FROM ${SNAPSHOT_SCHEMA_IDENT}.__sequences`);
1548
+ for (const { name, value } of seqs) await this.#pglite.exec(`SELECT setval(${name}, ${value})`);
1562
1549
  });
1563
- await pglite.exec("DISCARD ALL");
1550
+ await this.#pglite.exec("DISCARD ALL");
1564
1551
  }
1565
1552
  async #getTables() {
1566
1553
  const { rows } = await this.#pglite.query(`SELECT quote_ident(schemaname) || '.' || quote_ident(tablename) AS qualified
1567
1554
  FROM pg_tables
1568
1555
  WHERE ${USER_TABLES_WHERE}`);
1569
- return rows.length > 0 ? rows.map((row) => row.qualified).join(", ") : "";
1556
+ return rows.map((row) => row.qualified).join(", ");
1570
1557
  }
1571
1558
  /**
1572
1559
  * Self-heal: someone (e.g. `resetSchema`) may drop `_pglite_snapshot`
@@ -1865,7 +1852,8 @@ var PGliteServer = class {
1865
1852
  socket.duplex = new PGliteDuplex(this.pglite, {
1866
1853
  sessionLock: this.#sessionLock,
1867
1854
  timeout: this.#options.timeout,
1868
- syncToFs: resolveSyncToFs(this.pglite, this.#options.syncToFs)
1855
+ syncToFs: resolveSyncToFs(this.pglite, this.#options.syncToFs),
1856
+ rewriteSystemCatalogCharOids: false
1869
1857
  });
1870
1858
  socket.duplex.on("error", () => socket.destroy());
1871
1859
  socket.once("close", () => {
@@ -1911,16 +1899,43 @@ var PGliteServer = class {
1911
1899
  }
1912
1900
  };
1913
1901
  //#endregion
1902
+ //#region src/schema/pg18-not-null.ts
1903
+ const ENGINE_DENYLIST = "contype NOT IN ('p', 'u', 'f')";
1904
+ const PATCHED_DENYLIST = "contype NOT IN ('p', 'u', 'f', 'n')";
1905
+ const rewritePg18ConstraintsSql = (sql) => {
1906
+ if (!sql.includes("pg_constraint") || !sql.includes(ENGINE_DENYLIST)) return sql;
1907
+ return sql.replace(ENGINE_DENYLIST, PATCHED_DENYLIST);
1908
+ };
1909
+ /**
1910
+ * Proxies (rather than spreads) so class-based adapters keep working: every
1911
+ * untouched member is delegated with `this` bound to the original instance,
1912
+ * and members added in future @prisma/driver-adapter-utils versions pass
1913
+ * through without changes here.
1914
+ */
1915
+ const wrapAdapter = (adapter) => new Proxy(adapter, { get(target, prop) {
1916
+ if (prop === "queryRaw") return (query) => target.queryRaw({
1917
+ ...query,
1918
+ sql: rewritePg18ConstraintsSql(query.sql)
1919
+ });
1920
+ const value = Reflect.get(target, prop);
1921
+ return typeof value === "function" ? value.bind(target) : value;
1922
+ } });
1923
+ const wrapFactoryForPg18 = (factory) => new Proxy(factory, { get(target, prop) {
1924
+ if (prop === "connect") return async () => wrapAdapter(await target.connect());
1925
+ if (prop === "connectToShadowDb") return async () => wrapAdapter(await target.connectToShadowDb());
1926
+ const value = Reflect.get(target, prop);
1927
+ return typeof value === "function" ? value.bind(target) : value;
1928
+ } });
1929
+ //#endregion
1914
1930
  //#region src/schema/index.ts
1915
1931
  const bindAdapter = async (adapter) => {
1916
1932
  const { bindMigrationAwareSqlAdapterFactory } = await import("@prisma/driver-adapter-utils");
1917
- return bindMigrationAwareSqlAdapterFactory(adapter);
1933
+ return bindMigrationAwareSqlAdapterFactory(wrapFactoryForPg18(adapter));
1918
1934
  };
1919
1935
  const emptyFilter = () => ({
1920
1936
  externalTables: [],
1921
1937
  externalEnums: []
1922
1938
  });
1923
- const quoteIdent = (name) => `"${name.replace(/"/g, "\"\"")}"`;
1924
1939
  /**
1925
1940
  * Drop every non-system schema (and recreate `public`). Issued as raw SQL
1926
1941
  * through the adapter rather than `engine.reset(...)` because the engine only
@@ -2100,7 +2115,7 @@ const hasMigrations = async (pglite) => {
2100
2115
  const { rows } = await pglite.query(`SELECT to_regclass('public._prisma_migrations') IS NOT NULL AS exists`);
2101
2116
  if (!rows[0]?.exists) return false;
2102
2117
  const { rows: applied } = await pglite.query(`SELECT count(*)::int AS count FROM _prisma_migrations WHERE finished_at IS NOT NULL`);
2103
- return applied[0].count > 0;
2118
+ return (applied[0]?.count ?? 0) > 0;
2104
2119
  };
2105
2120
  /**
2106
2121
  * Returns `true` when the `public` schema contains at least one user table.