@atscript/db-postgres 0.1.145 → 0.1.147

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
@@ -79,6 +79,8 @@ const pgDialect = {
79
79
  },
80
80
  calendarBucket: pgCalendarBucket,
81
81
  jsonExtract: pgJsonExtract,
82
+ castDouble: (expr) => `CAST(${expr} AS DOUBLE PRECISION)`,
83
+ nullsSortLargest: true,
82
84
  createViewPrefix: "CREATE OR REPLACE VIEW",
83
85
  paramPlaceholder(index) {
84
86
  return `$${index}`;
@@ -105,33 +107,47 @@ function pgJsonExtract(quotedCol, path, type) {
105
107
  /**
106
108
  * Calendar-bucket label over an epoch-ms column (BIGINT for
107
109
  * `@db.default.now`, DOUBLE PRECISION otherwise): TEXT `'YYYY-MM-DD'` — the
108
- * local date of the bucket's first day in `b.tz` — or NULL for a NULL source
109
- * or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
110
+ * local date of the bucket's first day in `b.tz` — or, for `hour`,
111
+ * `'YYYY-MM-DDTHH:00'` — the local wall-clock hour — or NULL for a NULL
112
+ * source or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
110
113
  *
111
- * The local date is `(to_timestamp(col / 1000) AT TIME ZONE '<tz>')::date`:
112
- * `timestamptz AT TIME ZONE` yields the wall time in that zone, independent
113
- * of the session `TimeZone`. Truncation is calendar arithmetic on that date —
114
- * the week uses the generic `(isodow - weekStart + 7) % 7` formula
115
- * (`date_trunc('week')` is Monday-only). `date_trunc` and `to_char` get an
116
- * explicit `::timestamp`: a bare `date` resolves to their `timestamptz`
117
- * overloads, which would route through the session zone.
114
+ * The wall time is `to_timestamp(col / 1000) AT TIME ZONE '<tz>'`:
115
+ * `timestamptz AT TIME ZONE` yields the (zone-less) wall time in that zone,
116
+ * independent of the session `TimeZone` — so a `+05:30` zone's hour starts at
117
+ * :30 UTC and a DST fall-back's repeated hour is one label. The hour is
118
+ * formatted from it; the other units truncate its `::date` with calendar
119
+ * arithmetic — the week uses the generic `(isodow - weekStart + 7) % 7`
120
+ * formula (`date_trunc('week')` is Monday-only). `date_trunc` and `to_char`
121
+ * get an explicit `::timestamp`: a bare `date` resolves to their
122
+ * `timestamptz` overloads, which would route through the session zone.
118
123
  *
119
124
  * Parameter-free (the zone is an inlined, charset-checked literal), so the
120
125
  * SELECT and GROUP BY renderings match structurally.
121
126
  */
122
127
  function pgCalendarBucket(quotedCol, b) {
123
- const local = `(to_timestamp(${quotedCol}::double precision / 1000) AT TIME ZONE ${(0, _atscript_db_sql_tools.sqlTimeZoneLiteral)(b.tz)})::date`;
124
- let first;
128
+ const wall = `(to_timestamp(${quotedCol}::double precision / 1000) AT TIME ZONE ${(0, _atscript_db_sql_tools.sqlTimeZoneLiteral)(b.tz)})`;
129
+ const local = `${wall}::date`;
130
+ let value;
131
+ let format = "YYYY-MM-DD";
125
132
  switch (b.unit) {
133
+ case "hour":
134
+ value = wall;
135
+ format = "YYYY-MM-DD\"T\"HH24\":00\"";
136
+ break;
126
137
  case "day":
127
- first = local;
138
+ value = local;
128
139
  break;
129
140
  case "week":
130
- first = `(${local} - ((EXTRACT(ISODOW FROM ${local})::int - ${b.weekStartIso} + 7) % 7))`;
141
+ value = `(${local} - ((EXTRACT(ISODOW FROM ${local})::int - ${b.weekStartIso} + 7) % 7))`;
142
+ break;
143
+ case "month":
144
+ case "quarter":
145
+ case "year":
146
+ value = `date_trunc('${b.unit}', ${local}::timestamp)`;
131
147
  break;
132
- default: first = `date_trunc('${b.unit}', ${local}::timestamp)`;
148
+ default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
133
149
  }
134
- return `CASE WHEN ${quotedCol} >= ${_uniqu_core.BUCKET_MIN_INSTANT} AND ${quotedCol} < ${_uniqu_core.BUCKET_MAX_INSTANT} THEN ${`to_char(${first}::timestamp, 'YYYY-MM-DD')`} END`;
150
+ return `CASE WHEN ${quotedCol} >= ${_uniqu_core.BUCKET_MIN_INSTANT} AND ${quotedCol} < ${_uniqu_core.BUCKET_MAX_INSTANT} THEN ${`to_char(${value}::timestamp, '${format}')`} END`;
135
151
  }
136
152
  /**
137
153
  * Distance-in-meters expression from a geography column to a query point.
@@ -321,12 +337,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
321
337
  }
322
338
  if (foreignKeys) for (const fk of foreignKeys.values()) {
323
339
  if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
324
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
325
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
326
- let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
327
- if (fk.onDelete) constraint += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
328
- if (fk.onUpdate) constraint += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
329
- colDefs.push(constraint);
340
+ colDefs.push((0, _atscript_db_sql_tools.foreignKeySql)(qi, fk));
330
341
  }
331
342
  return `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${colDefs.join(", ")})`;
332
343
  }
@@ -341,9 +352,15 @@ function buildCreateTable(table, fields, foreignKeys, options) {
341
352
  *
342
353
  * Case-insensitive columns (`@db.collate 'nocase'`) are handled by CITEXT
343
354
  * column type at the storage level — no query-side wrapping needed.
355
+ *
356
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
357
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
358
+ * source table) — a statement with an aliased FROM must pass its alias.
359
+ *
360
+ * @param opts - since 0.1.147
344
361
  */
345
- function buildWhere(filter) {
346
- return (0, _atscript_db_sql_tools.buildWhere)(pgDialect, filter);
362
+ function buildWhere(filter, opts) {
363
+ return (0, _atscript_db_sql_tools.buildWhere)(pgDialect, filter, opts);
347
364
  }
348
365
  //#endregion
349
366
  //#region src/postgres-adapter.ts
@@ -426,7 +443,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
426
443
  _vectorThresholds = /* @__PURE__ */ new Map();
427
444
  /**
428
445
  * Schema name for catalog queries and qualification — `@db.schema` of the
429
- * bound table, or `null` (→ `'public'`, the same default the bound path
446
+ * bound table, or `null` (→ the connection's `current_schema()`, the same default the bound path
430
447
  * uses) when the table declares none or the adapter is an administrative
431
448
  * one with no readable (the name-taking schema-sync primitives run on such
432
449
  * an adapter).
@@ -482,6 +499,15 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
482
499
  _exec() {
483
500
  return this._txConnection() ?? this.driver;
484
501
  }
502
+ /**
503
+ * Relational predicates (`$some` / `$none`) render as correlated
504
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
505
+ *
506
+ * @since 0.1.147
507
+ */
508
+ supportsRelationFilters(_mode) {
509
+ return true;
510
+ }
485
511
  /** PostgreSQL enforces FK constraints natively. */
486
512
  supportsNativeForeignKeys() {
487
513
  return true;
@@ -506,6 +532,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
506
532
  aggregateFns() {
507
533
  return _atscript_db.ALL_AGGREGATE_FNS;
508
534
  }
535
+ /** Computed view columns and first-row joins. */
536
+ viewCapabilities() {
537
+ return _atscript_db.ALL_VIEW_CAPABILITIES;
538
+ }
509
539
  onBeforeFlatten(_type) {}
510
540
  onAfterFlatten() {
511
541
  for (const fd of this._table.fieldDescriptors) if (fd.collate === "nocase") this._nocaseColumns.add(fd.physicalName);
@@ -608,7 +638,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
608
638
  }
609
639
  async insertOne(data) {
610
640
  let { sql, params } = buildInsert(this.resolveTableName(), data);
611
- const pkCols = this._table.primaryKeys.map((pk) => qi(pk));
641
+ const pkCols = this._pkColumns().map((pk) => qi(pk));
612
642
  if (pkCols.length > 0) sql += ` RETURNING ${pkCols.join(", ")}`;
613
643
  this._log(sql, params);
614
644
  const returned = (await this._wrapConstraintError(() => this._exec().run(sql, params))).rows?.[0];
@@ -621,7 +651,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
621
651
  };
622
652
  return this.withTransaction(async () => {
623
653
  const tableName = this.resolveTableName();
624
- const pkCols = this._table.primaryKeys;
654
+ const pkCols = this._pkColumns();
625
655
  const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
626
656
  const columns = (0, _atscript_db_sql_tools.insertManyColumns)(data);
627
657
  const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
@@ -660,6 +690,16 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
660
690
  this._log(sql, params);
661
691
  return this._exec().all(sql, params);
662
692
  }
693
+ /**
694
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
695
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
696
+ */
697
+ async findManyPerPartition(query, partitionBy) {
698
+ const where = buildWhere(query.filter);
699
+ const { sql, params } = (0, _atscript_db_sql_tools.buildPartitionedSelect)(pgDialect, this.resolveTableName(), where, query.controls, partitionBy);
700
+ this._log(sql, params);
701
+ return (0, _atscript_db_sql_tools.stripPartitionRowNumber)(await this._exec().all(sql, params));
702
+ }
663
703
  async count(query) {
664
704
  const where = buildWhere(query.filter);
665
705
  const { sql, params } = (0, _atscript_db_sql_tools.finalizeParams)(pgDialect, {
@@ -699,6 +739,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
699
739
  throw error;
700
740
  }
701
741
  }
742
+ /** Physical primary-key columns (`@db.column` renames applied). */
743
+ _pkColumns() {
744
+ return this._table.primaryKeys.map((key) => this._table.physicalPath(key));
745
+ }
702
746
  /**
703
747
  * The predicate that narrows a single-row UPDATE/DELETE to the first row
704
748
  * matching `whereSql`: `<key> <op> (SELECT <cols> … LIMIT 1)`. Single-col
@@ -708,7 +752,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
708
752
  * existing behavior rather than guess.
709
753
  */
710
754
  _limitOnePredicate(quotedTable, whereSql) {
711
- const pkCols = this._table.primaryKeys;
755
+ const pkCols = this._pkColumns();
712
756
  const quotedKeys = pkCols.length > 0 ? pkCols.map((c) => qi(c)) : ["ctid"];
713
757
  const colList = quotedKeys.join(", ");
714
758
  return `${quotedKeys.length === 1 ? `${colList} =` : `(${colList}) IN`} (SELECT ${colList} FROM ${quotedTable} WHERE ${whereSql} LIMIT 1)`;
@@ -818,7 +862,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
818
862
  JOIN pg_class cl ON cl.oid = c.conrelid
819
863
  JOIN pg_class rcl ON rcl.oid = c.confrelid
820
864
  JOIN pg_namespace rn ON rn.oid = rcl.relnamespace
821
- WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, 'public')
865
+ WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, current_schema())
822
866
  ORDER BY cl.relname, c.conname`, [tableName, this._schema])).map((r) => ({
823
867
  table: r.table_name,
824
868
  fields: r.columns ?? [],
@@ -828,7 +872,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
828
872
  async getObjectKind(name) {
829
873
  switch ((await this._exec().get(`SELECT c.relkind FROM pg_class c
830
874
  JOIN pg_namespace n ON n.oid = c.relnamespace
831
- WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')
875
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, current_schema())
832
876
  AND c.relkind IN ('r', 'p', 'v', 'm')`, [name, this._schema]))?.relkind) {
833
877
  case "r":
834
878
  case "p": return "table";
@@ -856,7 +900,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
856
900
  const row = await this._exec().get(`SELECT c.conname FROM pg_constraint c
857
901
  JOIN pg_class cl ON cl.oid = c.conrelid
858
902
  JOIN pg_namespace n ON n.oid = cl.relnamespace
859
- WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [this._table.tableName, this._schema]);
903
+ WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [this._table.tableName, this._schema]);
860
904
  if (row?.conname) clauses.push(`DROP CONSTRAINT ${qi(row.conname)}`);
861
905
  }
862
906
  if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
@@ -888,8 +932,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
888
932
  ) AS is_pk
889
933
  FROM information_schema.columns c
890
934
  JOIN pg_attribute a ON a.attname = c.column_name
891
- AND a.attrelid = (SELECT oid FROM pg_class WHERE relname = $1 AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = COALESCE($2, 'public')))
892
- WHERE c.table_name = $1 AND c.table_schema = COALESCE($2, 'public')
935
+ AND a.attrelid = (SELECT oid FROM pg_class WHERE relname = $1 AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = COALESCE($2, current_schema())))
936
+ WHERE c.table_name = $1 AND c.table_schema = COALESCE($2, current_schema())
893
937
  ORDER BY c.ordinal_position`, [tableName, schema])).map((r) => {
894
938
  const column = {
895
939
  name: r.column_name,
@@ -1009,10 +1053,10 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1009
1053
  CROSS JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS k(attnum, refattnum, ord)
1010
1054
  JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
1011
1055
  JOIN pg_attribute ra ON ra.attrelid = c.confrelid AND ra.attnum = k.refattnum
1012
- WHERE c.contype = 'f' AND rcl.relname = $2 AND rn.nspname = COALESCE($1, 'public')
1056
+ WHERE c.contype = 'f' AND rcl.relname = $2 AND rn.nspname = COALESCE($1, current_schema())
1013
1057
  ORDER BY n.nspname, cl.relname, c.conname, k.ord`, [schema, this._table.tableName]);
1014
1058
  const inboundFks = /* @__PURE__ */ new Map();
1015
- const ownSchema = schema ?? "public";
1059
+ const ownSchema = schema ?? (await conn.get("SELECT current_schema() AS s"))?.s ?? "public";
1016
1060
  for (const fk of fkRefs) {
1017
1061
  if (fk.table_schema === ownSchema && fk.table_name === this._table.tableName) continue;
1018
1062
  const key = `${fk.table_schema}.${fk.table_name}.${fk.constraint_name}`;
@@ -1107,7 +1151,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1107
1151
  FROM pg_constraint c
1108
1152
  JOIN pg_class cl ON cl.oid = c.conrelid
1109
1153
  JOIN pg_namespace n ON n.oid = cl.relnamespace
1110
- WHERE cl.relname = $1 AND n.nspname = COALESCE($2, 'public')
1154
+ WHERE cl.relname = $1 AND n.nspname = COALESCE($2, current_schema())
1111
1155
  ORDER BY c.conname`, [table, this._schema]);
1112
1156
  const taken = new Set(rows.map((r) => r.conname));
1113
1157
  for (const { conname, contype, columns } of rows) {
@@ -1131,7 +1175,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1131
1175
  async _relationExists(conn, name) {
1132
1176
  return (await conn.get(`SELECT true AS present FROM pg_class c
1133
1177
  JOIN pg_namespace n ON n.oid = c.relnamespace
1134
- WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [name, this._schema]))?.present ?? false;
1178
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [name, this._schema]))?.present ?? false;
1135
1179
  }
1136
1180
  async afterSyncTable() {
1137
1181
  await this._resetIdentitySequences();
@@ -1154,7 +1198,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1154
1198
  }
1155
1199
  }
1156
1200
  async tableExists() {
1157
- return (await this._exec().get(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = COALESCE($2, 'public')) AS "exists"`, [this._table.tableName, this._schema]))?.exists ?? false;
1201
+ return (await this._exec().get(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = COALESCE($2, current_schema())) AS "exists"`, [this._table.tableName, this._schema]))?.exists ?? false;
1158
1202
  }
1159
1203
  /**
1160
1204
  * No CASCADE: when something outside the sync inventory (a user view, an
@@ -1183,7 +1227,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1183
1227
  JOIN pg_class i ON i.oid = x.indexrelid
1184
1228
  JOIN pg_namespace n ON n.oid = t.relnamespace
1185
1229
  JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(x.indkey)
1186
- WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')
1230
+ WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())
1187
1231
  AND i.relname LIKE 'atscript__%' AND a.attname = ANY($3)`, [
1188
1232
  tableName,
1189
1233
  schema,
@@ -1235,7 +1279,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1235
1279
  JOIN pg_class t ON t.oid = x.indrelid
1236
1280
  JOIN pg_class i ON i.oid = x.indexrelid
1237
1281
  JOIN pg_namespace n ON n.oid = t.relnamespace
1238
- WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [tableName, schema])).map((r) => ({
1282
+ WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [tableName, schema])).map((r) => ({
1239
1283
  name: r.name,
1240
1284
  columns: r.columns ?? void 0
1241
1285
  }));
@@ -1281,7 +1325,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1281
1325
  async syncForeignKeys() {
1282
1326
  const existingByName = await this._getExistingFkConstraints();
1283
1327
  const desiredFkKeys = /* @__PURE__ */ new Set();
1284
- for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].toSorted().join(","));
1328
+ for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(","));
1285
1329
  for (const [constraintName, columns] of existingByName) {
1286
1330
  const key = [...columns].toSorted().join(",");
1287
1331
  if (!desiredFkKeys.has(key)) {
@@ -1292,13 +1336,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1292
1336
  }
1293
1337
  const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
1294
1338
  for (const fk of this._table.foreignKeys.values()) {
1295
- const key = [...fk.fields].toSorted().join(",");
1339
+ const key = [...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(",");
1296
1340
  if (!existingKeys.has(key)) {
1297
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
1298
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
1299
- let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
1300
- if (fk.onDelete) ddl += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
1301
- if (fk.onUpdate) ddl += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
1341
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
1302
1342
  this._log(ddl);
1303
1343
  await this._exec().exec(ddl);
1304
1344
  }
@@ -1325,7 +1365,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1325
1365
  JOIN pg_namespace n ON n.oid = cl.relnamespace
1326
1366
  CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1327
1367
  JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
1328
- WHERE c.contype = 'f' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')
1368
+ WHERE c.contype = 'f' AND cl.relname = $1 AND n.nspname = COALESCE($2, current_schema())
1329
1369
  ORDER BY c.conname, k.ord`, [this._table.tableName, this._schema]);
1330
1370
  const byName = /* @__PURE__ */ new Map();
1331
1371
  for (const row of rows) {
@@ -1470,7 +1510,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1470
1510
  vec = first.value[1];
1471
1511
  }
1472
1512
  const distanceOp = similarityToPgOp(vec.similarity);
1473
- const where = buildWhere(query.filter);
1513
+ const where = buildWhere(query.filter, { qualifier: pgDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) });
1474
1514
  const controls = query.controls || {};
1475
1515
  const threshold = this._resolveVectorThreshold(controls, vec.indexName);
1476
1516
  return {
@@ -1563,7 +1603,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
1563
1603
  const controls = query.controls ?? {};
1564
1604
  return {
1565
1605
  tableName: this.resolveTableName(),
1566
- where: buildWhere(query.filter),
1606
+ where: buildWhere(query.filter, { qualifier: pgDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) }),
1567
1607
  dist: pgGeoDistanceExpr(qi(column), point),
1568
1608
  window: (0, _atscript_db_sql_tools.geoWindowFromControls)(controls),
1569
1609
  controls
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
- import { TSqlFragment } from "@atscript/db-sql-tools";
3
+ import { TFilterVisitorOptions, TSqlFragment } from "@atscript/db-sql-tools";
4
4
 
5
5
  //#region src/types.d.ts
6
6
  /**
@@ -89,7 +89,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
89
89
  private _vectorThresholds;
90
90
  /**
91
91
  * Schema name for catalog queries and qualification — `@db.schema` of the
92
- * bound table, or `null` (→ `'public'`, the same default the bound path
92
+ * bound table, or `null` (→ the connection's `current_schema()`, the same default the bound path
93
93
  * uses) when the table declares none or the adapter is an administrative
94
94
  * one with no readable (the name-taking schema-sync primitives run on such
95
95
  * an adapter).
@@ -108,6 +108,13 @@ declare class PostgresAdapter extends BaseDbAdapter {
108
108
  * otherwise the pool-based driver.
109
109
  */
110
110
  private _exec;
111
+ /**
112
+ * Relational predicates (`$some` / `$none`) render as correlated
113
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
114
+ *
115
+ * @since 0.1.147
116
+ */
117
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
111
118
  /** PostgreSQL enforces FK constraints natively. */
112
119
  supportsNativeForeignKeys(): boolean;
113
120
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -120,6 +127,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
120
127
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
121
128
  /** Every aggregate function, `countDistinct` included. */
122
129
  aggregateFns(): ReadonlySet<AggregateFn>;
130
+ /** Computed view columns and first-row joins. */
131
+ viewCapabilities(): ReadonlySet<TViewCapability>;
123
132
  onBeforeFlatten(_type: unknown): void;
124
133
  onAfterFlatten(): void;
125
134
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
@@ -146,6 +155,11 @@ declare class PostgresAdapter extends BaseDbAdapter {
146
155
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
147
156
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
148
157
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
158
+ /**
159
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
160
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
161
+ */
162
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
149
163
  count(query: DbQuery): Promise<number>;
150
164
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
151
165
  /**
@@ -156,6 +170,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
156
170
  * older than the runtime's. Other errors propagate unchanged.
157
171
  */
158
172
  private _wrapBucketZoneError;
173
+ /** Physical primary-key columns (`@db.column` renames applied). */
174
+ private _pkColumns;
159
175
  /**
160
176
  * The predicate that narrows a single-row UPDATE/DELETE to the first row
161
177
  * matching `whereSql`: `<key> <op> (SELECT <cols> … LIMIT 1)`. Single-col
@@ -342,8 +358,14 @@ declare class PgDriver implements TPgDriver {
342
358
  *
343
359
  * Case-insensitive columns (`@db.collate 'nocase'`) are handled by CITEXT
344
360
  * column type at the storage level — no query-side wrapping needed.
361
+ *
362
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
363
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
364
+ * source table) — a statement with an aliased FROM must pass its alias.
365
+ *
366
+ * @param opts - since 0.1.147
345
367
  */
346
- declare function buildWhere(filter: FilterExpr): TSqlFragment;
368
+ declare function buildWhere(filter: FilterExpr, opts?: TFilterVisitorOptions): TSqlFragment;
347
369
  //#endregion
348
370
  //#region src/index.d.ts
349
371
  /**
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
- import { TSqlFragment } from "@atscript/db-sql-tools";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
+ import { TFilterVisitorOptions, TSqlFragment } from "@atscript/db-sql-tools";
3
3
  import { TMetadataMap } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/types.d.ts
@@ -89,7 +89,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
89
89
  private _vectorThresholds;
90
90
  /**
91
91
  * Schema name for catalog queries and qualification — `@db.schema` of the
92
- * bound table, or `null` (→ `'public'`, the same default the bound path
92
+ * bound table, or `null` (→ the connection's `current_schema()`, the same default the bound path
93
93
  * uses) when the table declares none or the adapter is an administrative
94
94
  * one with no readable (the name-taking schema-sync primitives run on such
95
95
  * an adapter).
@@ -108,6 +108,13 @@ declare class PostgresAdapter extends BaseDbAdapter {
108
108
  * otherwise the pool-based driver.
109
109
  */
110
110
  private _exec;
111
+ /**
112
+ * Relational predicates (`$some` / `$none`) render as correlated
113
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
114
+ *
115
+ * @since 0.1.147
116
+ */
117
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
111
118
  /** PostgreSQL enforces FK constraints natively. */
112
119
  supportsNativeForeignKeys(): boolean;
113
120
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -120,6 +127,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
120
127
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
121
128
  /** Every aggregate function, `countDistinct` included. */
122
129
  aggregateFns(): ReadonlySet<AggregateFn>;
130
+ /** Computed view columns and first-row joins. */
131
+ viewCapabilities(): ReadonlySet<TViewCapability>;
123
132
  onBeforeFlatten(_type: unknown): void;
124
133
  onAfterFlatten(): void;
125
134
  onFieldScanned(field: string, _type: unknown, metadata: TMetadataMap<AtscriptMetadata>): void;
@@ -146,6 +155,11 @@ declare class PostgresAdapter extends BaseDbAdapter {
146
155
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
147
156
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
148
157
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
158
+ /**
159
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
160
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
161
+ */
162
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
149
163
  count(query: DbQuery): Promise<number>;
150
164
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
151
165
  /**
@@ -156,6 +170,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
156
170
  * older than the runtime's. Other errors propagate unchanged.
157
171
  */
158
172
  private _wrapBucketZoneError;
173
+ /** Physical primary-key columns (`@db.column` renames applied). */
174
+ private _pkColumns;
159
175
  /**
160
176
  * The predicate that narrows a single-row UPDATE/DELETE to the first row
161
177
  * matching `whereSql`: `<key> <op> (SELECT <cols> … LIMIT 1)`. Single-col
@@ -342,8 +358,14 @@ declare class PgDriver implements TPgDriver {
342
358
  *
343
359
  * Case-insensitive columns (`@db.collate 'nocase'`) are handled by CITEXT
344
360
  * column type at the storage level — no query-side wrapping needed.
361
+ *
362
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
363
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
364
+ * source table) — a statement with an aliased FROM must pass its alias.
365
+ *
366
+ * @param opts - since 0.1.147
345
367
  */
346
- declare function buildWhere(filter: FilterExpr): TSqlFragment;
368
+ declare function buildWhere(filter: FilterExpr, opts?: TFilterVisitorOptions): TSqlFragment;
347
369
  //#endregion
348
370
  //#region src/index.d.ts
349
371
  /**
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, isColumnTypeChanged, vectorIndexNotFoundMessage } from "@atscript/db";
1
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, fkColumns, isColumnTypeChanged, vectorIndexNotFoundMessage } from "@atscript/db";
2
2
  import { resolveAggregateSearch } from "@atscript/db/agg";
3
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, insertManyColumns, normalizeGeoPointValue, parseRegexString, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, vectorDistanceSource } from "@atscript/db-sql-tools";
3
+ import { SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, insertManyColumns, normalizeGeoPointValue, parseRegexString, quotedJsonPathSegments, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, vectorDistanceSource } from "@atscript/db-sql-tools";
4
4
  import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
5
5
  //#region src/sql-builder.ts
6
6
  /**
@@ -78,6 +78,8 @@ const pgDialect = {
78
78
  },
79
79
  calendarBucket: pgCalendarBucket,
80
80
  jsonExtract: pgJsonExtract,
81
+ castDouble: (expr) => `CAST(${expr} AS DOUBLE PRECISION)`,
82
+ nullsSortLargest: true,
81
83
  createViewPrefix: "CREATE OR REPLACE VIEW",
82
84
  paramPlaceholder(index) {
83
85
  return `$${index}`;
@@ -104,33 +106,47 @@ function pgJsonExtract(quotedCol, path, type) {
104
106
  /**
105
107
  * Calendar-bucket label over an epoch-ms column (BIGINT for
106
108
  * `@db.default.now`, DOUBLE PRECISION otherwise): TEXT `'YYYY-MM-DD'` — the
107
- * local date of the bucket's first day in `b.tz` — or NULL for a NULL source
108
- * or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
109
+ * local date of the bucket's first day in `b.tz` — or, for `hour`,
110
+ * `'YYYY-MM-DDTHH:00'` — the local wall-clock hour — or NULL for a NULL
111
+ * source or one outside `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
109
112
  *
110
- * The local date is `(to_timestamp(col / 1000) AT TIME ZONE '<tz>')::date`:
111
- * `timestamptz AT TIME ZONE` yields the wall time in that zone, independent
112
- * of the session `TimeZone`. Truncation is calendar arithmetic on that date —
113
- * the week uses the generic `(isodow - weekStart + 7) % 7` formula
114
- * (`date_trunc('week')` is Monday-only). `date_trunc` and `to_char` get an
115
- * explicit `::timestamp`: a bare `date` resolves to their `timestamptz`
116
- * overloads, which would route through the session zone.
113
+ * The wall time is `to_timestamp(col / 1000) AT TIME ZONE '<tz>'`:
114
+ * `timestamptz AT TIME ZONE` yields the (zone-less) wall time in that zone,
115
+ * independent of the session `TimeZone` — so a `+05:30` zone's hour starts at
116
+ * :30 UTC and a DST fall-back's repeated hour is one label. The hour is
117
+ * formatted from it; the other units truncate its `::date` with calendar
118
+ * arithmetic — the week uses the generic `(isodow - weekStart + 7) % 7`
119
+ * formula (`date_trunc('week')` is Monday-only). `date_trunc` and `to_char`
120
+ * get an explicit `::timestamp`: a bare `date` resolves to their
121
+ * `timestamptz` overloads, which would route through the session zone.
117
122
  *
118
123
  * Parameter-free (the zone is an inlined, charset-checked literal), so the
119
124
  * SELECT and GROUP BY renderings match structurally.
120
125
  */
121
126
  function pgCalendarBucket(quotedCol, b) {
122
- const local = `(to_timestamp(${quotedCol}::double precision / 1000) AT TIME ZONE ${sqlTimeZoneLiteral(b.tz)})::date`;
123
- let first;
127
+ const wall = `(to_timestamp(${quotedCol}::double precision / 1000) AT TIME ZONE ${sqlTimeZoneLiteral(b.tz)})`;
128
+ const local = `${wall}::date`;
129
+ let value;
130
+ let format = "YYYY-MM-DD";
124
131
  switch (b.unit) {
132
+ case "hour":
133
+ value = wall;
134
+ format = "YYYY-MM-DD\"T\"HH24\":00\"";
135
+ break;
125
136
  case "day":
126
- first = local;
137
+ value = local;
127
138
  break;
128
139
  case "week":
129
- first = `(${local} - ((EXTRACT(ISODOW FROM ${local})::int - ${b.weekStartIso} + 7) % 7))`;
140
+ value = `(${local} - ((EXTRACT(ISODOW FROM ${local})::int - ${b.weekStartIso} + 7) % 7))`;
141
+ break;
142
+ case "month":
143
+ case "quarter":
144
+ case "year":
145
+ value = `date_trunc('${b.unit}', ${local}::timestamp)`;
130
146
  break;
131
- default: first = `date_trunc('${b.unit}', ${local}::timestamp)`;
147
+ default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
132
148
  }
133
- return `CASE WHEN ${quotedCol} >= ${BUCKET_MIN_INSTANT} AND ${quotedCol} < ${BUCKET_MAX_INSTANT} THEN ${`to_char(${first}::timestamp, 'YYYY-MM-DD')`} END`;
149
+ return `CASE WHEN ${quotedCol} >= ${BUCKET_MIN_INSTANT} AND ${quotedCol} < ${BUCKET_MAX_INSTANT} THEN ${`to_char(${value}::timestamp, '${format}')`} END`;
134
150
  }
135
151
  /**
136
152
  * Distance-in-meters expression from a geography column to a query point.
@@ -320,12 +336,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
320
336
  }
321
337
  if (foreignKeys) for (const fk of foreignKeys.values()) {
322
338
  if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
323
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
324
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
325
- let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
326
- if (fk.onDelete) constraint += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
327
- if (fk.onUpdate) constraint += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
328
- colDefs.push(constraint);
339
+ colDefs.push(foreignKeySql(qi, fk));
329
340
  }
330
341
  return `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${colDefs.join(", ")})`;
331
342
  }
@@ -340,9 +351,15 @@ function buildCreateTable(table, fields, foreignKeys, options) {
340
351
  *
341
352
  * Case-insensitive columns (`@db.collate 'nocase'`) are handled by CITEXT
342
353
  * column type at the storage level — no query-side wrapping needed.
354
+ *
355
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
356
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
357
+ * source table) — a statement with an aliased FROM must pass its alias.
358
+ *
359
+ * @param opts - since 0.1.147
343
360
  */
344
- function buildWhere(filter) {
345
- return buildWhere$1(pgDialect, filter);
361
+ function buildWhere(filter, opts) {
362
+ return buildWhere$1(pgDialect, filter, opts);
346
363
  }
347
364
  //#endregion
348
365
  //#region src/postgres-adapter.ts
@@ -425,7 +442,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
425
442
  _vectorThresholds = /* @__PURE__ */ new Map();
426
443
  /**
427
444
  * Schema name for catalog queries and qualification — `@db.schema` of the
428
- * bound table, or `null` (→ `'public'`, the same default the bound path
445
+ * bound table, or `null` (→ the connection's `current_schema()`, the same default the bound path
429
446
  * uses) when the table declares none or the adapter is an administrative
430
447
  * one with no readable (the name-taking schema-sync primitives run on such
431
448
  * an adapter).
@@ -481,6 +498,15 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
481
498
  _exec() {
482
499
  return this._txConnection() ?? this.driver;
483
500
  }
501
+ /**
502
+ * Relational predicates (`$some` / `$none`) render as correlated
503
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
504
+ *
505
+ * @since 0.1.147
506
+ */
507
+ supportsRelationFilters(_mode) {
508
+ return true;
509
+ }
484
510
  /** PostgreSQL enforces FK constraints natively. */
485
511
  supportsNativeForeignKeys() {
486
512
  return true;
@@ -505,6 +531,10 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
505
531
  aggregateFns() {
506
532
  return ALL_AGGREGATE_FNS;
507
533
  }
534
+ /** Computed view columns and first-row joins. */
535
+ viewCapabilities() {
536
+ return ALL_VIEW_CAPABILITIES;
537
+ }
508
538
  onBeforeFlatten(_type) {}
509
539
  onAfterFlatten() {
510
540
  for (const fd of this._table.fieldDescriptors) if (fd.collate === "nocase") this._nocaseColumns.add(fd.physicalName);
@@ -607,7 +637,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
607
637
  }
608
638
  async insertOne(data) {
609
639
  let { sql, params } = buildInsert$1(this.resolveTableName(), data);
610
- const pkCols = this._table.primaryKeys.map((pk) => qi(pk));
640
+ const pkCols = this._pkColumns().map((pk) => qi(pk));
611
641
  if (pkCols.length > 0) sql += ` RETURNING ${pkCols.join(", ")}`;
612
642
  this._log(sql, params);
613
643
  const returned = (await this._wrapConstraintError(() => this._exec().run(sql, params))).rows?.[0];
@@ -620,7 +650,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
620
650
  };
621
651
  return this.withTransaction(async () => {
622
652
  const tableName = this.resolveTableName();
623
- const pkCols = this._table.primaryKeys;
653
+ const pkCols = this._pkColumns();
624
654
  const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
625
655
  const columns = insertManyColumns(data);
626
656
  const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
@@ -659,6 +689,16 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
659
689
  this._log(sql, params);
660
690
  return this._exec().all(sql, params);
661
691
  }
692
+ /**
693
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
694
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
695
+ */
696
+ async findManyPerPartition(query, partitionBy) {
697
+ const where = buildWhere(query.filter);
698
+ const { sql, params } = buildPartitionedSelect(pgDialect, this.resolveTableName(), where, query.controls, partitionBy);
699
+ this._log(sql, params);
700
+ return stripPartitionRowNumber(await this._exec().all(sql, params));
701
+ }
662
702
  async count(query) {
663
703
  const where = buildWhere(query.filter);
664
704
  const { sql, params } = finalizeParams(pgDialect, {
@@ -698,6 +738,10 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
698
738
  throw error;
699
739
  }
700
740
  }
741
+ /** Physical primary-key columns (`@db.column` renames applied). */
742
+ _pkColumns() {
743
+ return this._table.primaryKeys.map((key) => this._table.physicalPath(key));
744
+ }
701
745
  /**
702
746
  * The predicate that narrows a single-row UPDATE/DELETE to the first row
703
747
  * matching `whereSql`: `<key> <op> (SELECT <cols> … LIMIT 1)`. Single-col
@@ -707,7 +751,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
707
751
  * existing behavior rather than guess.
708
752
  */
709
753
  _limitOnePredicate(quotedTable, whereSql) {
710
- const pkCols = this._table.primaryKeys;
754
+ const pkCols = this._pkColumns();
711
755
  const quotedKeys = pkCols.length > 0 ? pkCols.map((c) => qi(c)) : ["ctid"];
712
756
  const colList = quotedKeys.join(", ");
713
757
  return `${quotedKeys.length === 1 ? `${colList} =` : `(${colList}) IN`} (SELECT ${colList} FROM ${quotedTable} WHERE ${whereSql} LIMIT 1)`;
@@ -817,7 +861,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
817
861
  JOIN pg_class cl ON cl.oid = c.conrelid
818
862
  JOIN pg_class rcl ON rcl.oid = c.confrelid
819
863
  JOIN pg_namespace rn ON rn.oid = rcl.relnamespace
820
- WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, 'public')
864
+ WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, current_schema())
821
865
  ORDER BY cl.relname, c.conname`, [tableName, this._schema])).map((r) => ({
822
866
  table: r.table_name,
823
867
  fields: r.columns ?? [],
@@ -827,7 +871,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
827
871
  async getObjectKind(name) {
828
872
  switch ((await this._exec().get(`SELECT c.relkind FROM pg_class c
829
873
  JOIN pg_namespace n ON n.oid = c.relnamespace
830
- WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')
874
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, current_schema())
831
875
  AND c.relkind IN ('r', 'p', 'v', 'm')`, [name, this._schema]))?.relkind) {
832
876
  case "r":
833
877
  case "p": return "table";
@@ -855,7 +899,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
855
899
  const row = await this._exec().get(`SELECT c.conname FROM pg_constraint c
856
900
  JOIN pg_class cl ON cl.oid = c.conrelid
857
901
  JOIN pg_namespace n ON n.oid = cl.relnamespace
858
- WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [this._table.tableName, this._schema]);
902
+ WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [this._table.tableName, this._schema]);
859
903
  if (row?.conname) clauses.push(`DROP CONSTRAINT ${qi(row.conname)}`);
860
904
  }
861
905
  if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
@@ -887,8 +931,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
887
931
  ) AS is_pk
888
932
  FROM information_schema.columns c
889
933
  JOIN pg_attribute a ON a.attname = c.column_name
890
- AND a.attrelid = (SELECT oid FROM pg_class WHERE relname = $1 AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = COALESCE($2, 'public')))
891
- WHERE c.table_name = $1 AND c.table_schema = COALESCE($2, 'public')
934
+ AND a.attrelid = (SELECT oid FROM pg_class WHERE relname = $1 AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = COALESCE($2, current_schema())))
935
+ WHERE c.table_name = $1 AND c.table_schema = COALESCE($2, current_schema())
892
936
  ORDER BY c.ordinal_position`, [tableName, schema])).map((r) => {
893
937
  const column = {
894
938
  name: r.column_name,
@@ -1008,10 +1052,10 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1008
1052
  CROSS JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS k(attnum, refattnum, ord)
1009
1053
  JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
1010
1054
  JOIN pg_attribute ra ON ra.attrelid = c.confrelid AND ra.attnum = k.refattnum
1011
- WHERE c.contype = 'f' AND rcl.relname = $2 AND rn.nspname = COALESCE($1, 'public')
1055
+ WHERE c.contype = 'f' AND rcl.relname = $2 AND rn.nspname = COALESCE($1, current_schema())
1012
1056
  ORDER BY n.nspname, cl.relname, c.conname, k.ord`, [schema, this._table.tableName]);
1013
1057
  const inboundFks = /* @__PURE__ */ new Map();
1014
- const ownSchema = schema ?? "public";
1058
+ const ownSchema = schema ?? (await conn.get("SELECT current_schema() AS s"))?.s ?? "public";
1015
1059
  for (const fk of fkRefs) {
1016
1060
  if (fk.table_schema === ownSchema && fk.table_name === this._table.tableName) continue;
1017
1061
  const key = `${fk.table_schema}.${fk.table_name}.${fk.constraint_name}`;
@@ -1106,7 +1150,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1106
1150
  FROM pg_constraint c
1107
1151
  JOIN pg_class cl ON cl.oid = c.conrelid
1108
1152
  JOIN pg_namespace n ON n.oid = cl.relnamespace
1109
- WHERE cl.relname = $1 AND n.nspname = COALESCE($2, 'public')
1153
+ WHERE cl.relname = $1 AND n.nspname = COALESCE($2, current_schema())
1110
1154
  ORDER BY c.conname`, [table, this._schema]);
1111
1155
  const taken = new Set(rows.map((r) => r.conname));
1112
1156
  for (const { conname, contype, columns } of rows) {
@@ -1130,7 +1174,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1130
1174
  async _relationExists(conn, name) {
1131
1175
  return (await conn.get(`SELECT true AS present FROM pg_class c
1132
1176
  JOIN pg_namespace n ON n.oid = c.relnamespace
1133
- WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [name, this._schema]))?.present ?? false;
1177
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [name, this._schema]))?.present ?? false;
1134
1178
  }
1135
1179
  async afterSyncTable() {
1136
1180
  await this._resetIdentitySequences();
@@ -1153,7 +1197,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1153
1197
  }
1154
1198
  }
1155
1199
  async tableExists() {
1156
- return (await this._exec().get(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = COALESCE($2, 'public')) AS "exists"`, [this._table.tableName, this._schema]))?.exists ?? false;
1200
+ return (await this._exec().get(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = COALESCE($2, current_schema())) AS "exists"`, [this._table.tableName, this._schema]))?.exists ?? false;
1157
1201
  }
1158
1202
  /**
1159
1203
  * No CASCADE: when something outside the sync inventory (a user view, an
@@ -1182,7 +1226,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1182
1226
  JOIN pg_class i ON i.oid = x.indexrelid
1183
1227
  JOIN pg_namespace n ON n.oid = t.relnamespace
1184
1228
  JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(x.indkey)
1185
- WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')
1229
+ WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())
1186
1230
  AND i.relname LIKE 'atscript__%' AND a.attname = ANY($3)`, [
1187
1231
  tableName,
1188
1232
  schema,
@@ -1234,7 +1278,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1234
1278
  JOIN pg_class t ON t.oid = x.indrelid
1235
1279
  JOIN pg_class i ON i.oid = x.indexrelid
1236
1280
  JOIN pg_namespace n ON n.oid = t.relnamespace
1237
- WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [tableName, schema])).map((r) => ({
1281
+ WHERE t.relname = $1 AND n.nspname = COALESCE($2, current_schema())`, [tableName, schema])).map((r) => ({
1238
1282
  name: r.name,
1239
1283
  columns: r.columns ?? void 0
1240
1284
  }));
@@ -1280,7 +1324,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1280
1324
  async syncForeignKeys() {
1281
1325
  const existingByName = await this._getExistingFkConstraints();
1282
1326
  const desiredFkKeys = /* @__PURE__ */ new Set();
1283
- for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].toSorted().join(","));
1327
+ for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fkColumns(fk).fields].toSorted().join(","));
1284
1328
  for (const [constraintName, columns] of existingByName) {
1285
1329
  const key = [...columns].toSorted().join(",");
1286
1330
  if (!desiredFkKeys.has(key)) {
@@ -1291,13 +1335,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1291
1335
  }
1292
1336
  const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
1293
1337
  for (const fk of this._table.foreignKeys.values()) {
1294
- const key = [...fk.fields].toSorted().join(",");
1338
+ const key = [...fkColumns(fk).fields].toSorted().join(",");
1295
1339
  if (!existingKeys.has(key)) {
1296
- const localCols = fk.fields.map((f) => qi(f)).join(", ");
1297
- const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
1298
- let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
1299
- if (fk.onDelete) ddl += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
1300
- if (fk.onUpdate) ddl += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
1340
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${foreignKeySql(qi, fk)}`;
1301
1341
  this._log(ddl);
1302
1342
  await this._exec().exec(ddl);
1303
1343
  }
@@ -1324,7 +1364,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1324
1364
  JOIN pg_namespace n ON n.oid = cl.relnamespace
1325
1365
  CROSS JOIN LATERAL unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1326
1366
  JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
1327
- WHERE c.contype = 'f' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')
1367
+ WHERE c.contype = 'f' AND cl.relname = $1 AND n.nspname = COALESCE($2, current_schema())
1328
1368
  ORDER BY c.conname, k.ord`, [this._table.tableName, this._schema]);
1329
1369
  const byName = /* @__PURE__ */ new Map();
1330
1370
  for (const row of rows) {
@@ -1469,7 +1509,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1469
1509
  vec = first.value[1];
1470
1510
  }
1471
1511
  const distanceOp = similarityToPgOp(vec.similarity);
1472
- const where = buildWhere(query.filter);
1512
+ const where = buildWhere(query.filter, { qualifier: pgDialect.quoteTable(SEARCH_SOURCE_ALIAS) });
1473
1513
  const controls = query.controls || {};
1474
1514
  const threshold = this._resolveVectorThreshold(controls, vec.indexName);
1475
1515
  return {
@@ -1562,7 +1602,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
1562
1602
  const controls = query.controls ?? {};
1563
1603
  return {
1564
1604
  tableName: this.resolveTableName(),
1565
- where: buildWhere(query.filter),
1605
+ where: buildWhere(query.filter, { qualifier: pgDialect.quoteTable(SEARCH_SOURCE_ALIAS) }),
1566
1606
  dist: pgGeoDistanceExpr(qi(column), point),
1567
1607
  window: geoWindowFromControls(controls),
1568
1608
  controls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-postgres",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "PostgreSQL adapter for @atscript/db with pg (node-postgres) driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -43,20 +43,20 @@
43
43
  "access": "public"
44
44
  },
45
45
  "devDependencies": {
46
- "@atscript/core": "^0.1.97",
47
- "@atscript/typescript": "^0.1.97",
46
+ "@atscript/core": "^0.1.99",
47
+ "@atscript/typescript": "^0.1.99",
48
48
  "@types/pg": "^8.11.0",
49
- "@uniqu/core": "^0.1.11",
49
+ "@uniqu/core": "^0.1.12",
50
50
  "pg": "^8.13.0",
51
- "unplugin-atscript": "^0.1.97"
51
+ "unplugin-atscript": "^0.1.99"
52
52
  },
53
53
  "peerDependencies": {
54
- "@atscript/core": "^0.1.97",
55
- "@atscript/typescript": "^0.1.97",
56
- "@uniqu/core": "^0.1.11",
54
+ "@atscript/core": "^0.1.99",
55
+ "@atscript/typescript": "^0.1.99",
56
+ "@uniqu/core": "^0.1.12",
57
57
  "pg": ">=8.0.0",
58
- "@atscript/db": "^0.1.145",
59
- "@atscript/db-sql-tools": "^0.1.145"
58
+ "@atscript/db": "^0.1.147",
59
+ "@atscript/db-sql-tools": "^0.1.147"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "pg": {