@atscript/db-postgres 0.1.127 → 0.1.128

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
@@ -241,6 +241,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
241
241
  colDefs.push(`PRIMARY KEY (${pkCols})`);
242
242
  }
243
243
  if (foreignKeys) for (const fk of foreignKeys.values()) {
244
+ if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
244
245
  const localCols = fk.fields.map((f) => qi(f)).join(", ");
245
246
  const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
246
247
  let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
@@ -321,14 +322,28 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
321
322
  _vectorFields = /* @__PURE__ */ new Map();
322
323
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
323
324
  _vectorThresholds = /* @__PURE__ */ new Map();
324
- /** Schema name for queries (null falls through to 'public'). */
325
+ /**
326
+ * Schema name for catalog queries and qualification — `@db.schema` of the
327
+ * bound table, or `null` (→ `'public'`, the same default the bound path
328
+ * uses) when the table declares none or the adapter is an administrative
329
+ * one with no readable (the name-taking schema-sync primitives run on such
330
+ * an adapter).
331
+ */
325
332
  get _schema() {
326
- return this._table.schema ?? null;
333
+ return this._table?.schema ?? null;
327
334
  }
328
335
  constructor(driver) {
329
336
  super();
330
337
  this.driver = driver;
331
338
  }
339
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
340
+ _transactionOwner() {
341
+ return this.driver;
342
+ }
343
+ /** The dedicated connection of this pool's open transaction, if any. */
344
+ _txConnection() {
345
+ return this._getTransactionState();
346
+ }
332
347
  async _beginTransaction() {
333
348
  const conn = await this.driver.getConnection();
334
349
  try {
@@ -363,7 +378,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
363
378
  * otherwise the pool-based driver.
364
379
  */
365
380
  _exec() {
366
- return this._getTransactionState() ?? this.driver;
381
+ return this._txConnection() ?? this.driver;
367
382
  }
368
383
  /** PostgreSQL enforces FK constraints natively. */
369
384
  supportsNativeForeignKeys() {
@@ -597,7 +612,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
597
612
  };
598
613
  }
599
614
  async replaceOne(filter, data, expectedVersion) {
600
- return this.updateOne(filter, data, void 0, expectedVersion);
615
+ const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), this._table.versionColumn);
616
+ return this.updateOne(filter, full, void 0, expectedVersion);
601
617
  }
602
618
  async replaceMany(filter, data) {
603
619
  return this.updateMany(filter, data);
@@ -626,7 +642,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
626
642
  _hasGeoPointFields() {
627
643
  return this._table.fieldDescriptors.some((fd) => fd.isGeoPoint && !fd.encrypted);
628
644
  }
629
- async ensureTable() {
645
+ async ensureTable(opts) {
630
646
  if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
631
647
  await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
632
648
  this._citextProvisioned = true;
@@ -636,15 +652,89 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
636
652
  }
637
653
  await this.prepareTypeMapper();
638
654
  if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
639
- if (this._table instanceof _atscript_db.AtscriptDbView) return this._ensureView();
655
+ if (this._table.isView) return this._ensureView();
640
656
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
641
657
  incrementFields: this._incrementFields,
642
658
  autoIncrementStart: this._autoIncrementStart,
643
- typeMapper: (field) => this.typeMapper(field)
659
+ typeMapper: (field) => this.typeMapper(field),
660
+ deferForeignKeysTo: opts?.deferForeignKeysTo
644
661
  });
645
662
  this._log(sql);
646
663
  await this._exec().exec(sql);
647
664
  }
665
+ /** `schema.name` when the adapter targets a named schema. */
666
+ _qualify(name) {
667
+ return quoteTableName(this._schema ? `${this._schema}.${name}` : name);
668
+ }
669
+ async hasRows(tableName) {
670
+ const sql = `SELECT EXISTS (SELECT 1 FROM ${tableName ? this._qualify(tableName) : quoteTableName(this.resolveTableName())}) AS "present"`;
671
+ this._log(sql);
672
+ return (await this._exec().get(sql, []))?.present ?? false;
673
+ }
674
+ /**
675
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
676
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
677
+ * `key_column_usage` ordinal join).
678
+ */
679
+ async getReferencingForeignKeys(tableName) {
680
+ return (await this._exec().all(`SELECT cl.relname AS table_name, c.conname AS constraint_name,
681
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
682
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
683
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS columns,
684
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
685
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
686
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_columns
687
+ FROM pg_constraint c
688
+ JOIN pg_class cl ON cl.oid = c.conrelid
689
+ JOIN pg_class rcl ON rcl.oid = c.confrelid
690
+ JOIN pg_namespace rn ON rn.oid = rcl.relnamespace
691
+ WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, 'public')
692
+ ORDER BY cl.relname, c.conname`, [tableName, this._schema])).map((r) => ({
693
+ table: r.table_name,
694
+ fields: r.columns ?? [],
695
+ targetFields: r.ref_columns ?? []
696
+ }));
697
+ }
698
+ async getObjectKind(name) {
699
+ switch ((await this._exec().get(`SELECT c.relkind FROM pg_class c
700
+ JOIN pg_namespace n ON n.oid = c.relnamespace
701
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')
702
+ AND c.relkind IN ('r', 'p', 'v', 'm')`, [name, this._schema]))?.relkind) {
703
+ case "r":
704
+ case "p": return "table";
705
+ case "v": return "view";
706
+ case "m": return "materialized";
707
+ default: return;
708
+ }
709
+ }
710
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
711
+ async dropTablesByName(tableNames) {
712
+ if (tableNames.length === 0) return;
713
+ const ddl = `DROP TABLE IF EXISTS ${tableNames.map((n) => this._qualify(n)).join(", ")}`;
714
+ this._log(ddl);
715
+ await this._exec().exec(ddl);
716
+ }
717
+ /**
718
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
719
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
720
+ * key columns NOT NULL. A demoted identity column is handled by the
721
+ * `defaultChanged` diff (`DROP IDENTITY`).
722
+ */
723
+ async rebuildPrimaryKey(change) {
724
+ const clauses = [];
725
+ if (change.from.length > 0) {
726
+ const row = await this._exec().get(`SELECT c.conname FROM pg_constraint c
727
+ JOIN pg_class cl ON cl.oid = c.conrelid
728
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
729
+ WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [this._table.tableName, this._schema]);
730
+ if (row?.conname) clauses.push(`DROP CONSTRAINT ${qi(row.conname)}`);
731
+ }
732
+ if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
733
+ if (clauses.length === 0) return;
734
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ${clauses.join(", ")}`;
735
+ this._log(ddl);
736
+ await this._exec().exec(ddl);
737
+ }
648
738
  async _ensureView() {
649
739
  const view = this._table;
650
740
  const sql = buildCreateView(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref, qi));
@@ -734,15 +824,27 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
734
824
  this._log(ddl);
735
825
  await this._exec().exec(ddl);
736
826
  }
737
- for (const { field } of diff.defaultChanged ?? []) {
738
- let ddl;
739
- if (field.defaultValue?.kind === "value") ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
740
- else if (field.defaultValue?.kind === "fn") {
741
- const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : field.defaultValue.fn === "uuid" ? "gen_random_uuid()" : `${field.defaultValue.fn}()`;
742
- ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${fnExpr}`;
743
- } else ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
744
- this._log(ddl);
745
- await this._exec().exec(ddl);
827
+ for (const { field, oldDefault } of diff.defaultChanged ?? []) {
828
+ const col = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)}`;
829
+ const wasIdentity = oldDefault === "fn:increment";
830
+ const statements = [];
831
+ if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") {
832
+ if (!wasIdentity) {
833
+ if (oldDefault !== void 0) statements.push(`${col} DROP DEFAULT`);
834
+ statements.push(`${col} SET NOT NULL`, `${col} ADD GENERATED BY DEFAULT AS IDENTITY`);
835
+ }
836
+ } else {
837
+ if (wasIdentity) statements.push(`${col} DROP IDENTITY IF EXISTS`);
838
+ if (field.defaultValue?.kind === "value") statements.push(`${col} SET DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`);
839
+ else if (field.defaultValue?.kind === "fn") {
840
+ const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : "gen_random_uuid()";
841
+ statements.push(`${col} SET DEFAULT ${fnExpr}`);
842
+ } else if (!wasIdentity) statements.push(`${col} DROP DEFAULT`);
843
+ }
844
+ for (const ddl of statements) {
845
+ this._log(ddl);
846
+ await this._exec().exec(ddl);
847
+ }
746
848
  }
747
849
  return {
748
850
  added,
@@ -867,8 +969,14 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
867
969
  async tableExists() {
868
970
  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;
869
971
  }
972
+ /**
973
+ * No CASCADE: when something outside the sync inventory (a user view, an
974
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
975
+ * sync reports that as an error entry instead of silently dropping the
976
+ * dependents.
977
+ */
870
978
  async dropTable() {
871
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())} CASCADE`;
979
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
872
980
  this._log(ddl);
873
981
  await this._exec().exec(ddl);
874
982
  }
@@ -901,7 +1009,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
901
1009
  }
902
1010
  }
903
1011
  async dropTableByName(tableName) {
904
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
1012
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
905
1013
  this._log(ddl);
906
1014
  await this._exec().exec(ddl);
907
1015
  }
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TExistingColumn, TFieldOps, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
3
  import { TSqlFragment } from "@atscript/db-sql-tools";
4
4
 
@@ -103,9 +103,19 @@ declare class PostgresAdapter extends BaseDbAdapter {
103
103
  private _vectorFields;
104
104
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
105
105
  private _vectorThresholds;
106
- /** Schema name for queries (null falls through to 'public'). */
106
+ /**
107
+ * Schema name for catalog queries and qualification — `@db.schema` of the
108
+ * bound table, or `null` (→ `'public'`, the same default the bound path
109
+ * uses) when the table declares none or the adapter is an administrative
110
+ * one with no readable (the name-taking schema-sync primitives run on such
111
+ * an adapter).
112
+ */
107
113
  private get _schema();
108
114
  constructor(driver: TPgDriver);
115
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
116
+ protected _transactionOwner(): unknown;
117
+ /** The dedicated connection of this pool's open transaction, if any. */
118
+ private _txConnection;
109
119
  protected _beginTransaction(): Promise<TPgConnection>;
110
120
  protected _commitTransaction(state: unknown): Promise<void>;
111
121
  protected _rollbackTransaction(state: unknown): Promise<void>;
@@ -156,7 +166,26 @@ declare class PostgresAdapter extends BaseDbAdapter {
156
166
  prepareTypeMapper(): Promise<void>;
157
167
  /** Whether the table declares any `db.geoPoint` fields (unencrypted). */
158
168
  private _hasGeoPointFields;
159
- ensureTable(): Promise<void>;
169
+ ensureTable(opts?: TEnsureTableOptions): Promise<void>;
170
+ /** `schema.name` when the adapter targets a named schema. */
171
+ private _qualify;
172
+ hasRows(tableName?: string): Promise<boolean>;
173
+ /**
174
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
175
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
176
+ * `key_column_usage` ordinal join).
177
+ */
178
+ getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
179
+ getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
180
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
181
+ dropTablesByName(tableNames: string[]): Promise<void>;
182
+ /**
183
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
184
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
185
+ * key columns NOT NULL. A demoted identity column is handled by the
186
+ * `defaultChanged` diff (`DROP IDENTITY`).
187
+ */
188
+ rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
160
189
  private _ensureView;
161
190
  getExistingColumns(): Promise<TExistingColumn[]>;
162
191
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
@@ -171,6 +200,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
171
200
  */
172
201
  private _resetIdentitySequences;
173
202
  tableExists(): Promise<boolean>;
203
+ /**
204
+ * No CASCADE: when something outside the sync inventory (a user view, an
205
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
206
+ * sync reports that as an error entry instead of silently dropping the
207
+ * dependents.
208
+ */
174
209
  dropTable(): Promise<void>;
175
210
  dropColumns(columns: string[]): Promise<void>;
176
211
  dropIndexesForColumns(columns: string[]): Promise<void>;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TExistingColumn, TFieldOps, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
2
  import { TSqlFragment } from "@atscript/db-sql-tools";
3
3
  import { TMetadataMap } from "@atscript/typescript/utils";
4
4
 
@@ -103,9 +103,19 @@ declare class PostgresAdapter extends BaseDbAdapter {
103
103
  private _vectorFields;
104
104
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
105
105
  private _vectorThresholds;
106
- /** Schema name for queries (null falls through to 'public'). */
106
+ /**
107
+ * Schema name for catalog queries and qualification — `@db.schema` of the
108
+ * bound table, or `null` (→ `'public'`, the same default the bound path
109
+ * uses) when the table declares none or the adapter is an administrative
110
+ * one with no readable (the name-taking schema-sync primitives run on such
111
+ * an adapter).
112
+ */
107
113
  private get _schema();
108
114
  constructor(driver: TPgDriver);
115
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
116
+ protected _transactionOwner(): unknown;
117
+ /** The dedicated connection of this pool's open transaction, if any. */
118
+ private _txConnection;
109
119
  protected _beginTransaction(): Promise<TPgConnection>;
110
120
  protected _commitTransaction(state: unknown): Promise<void>;
111
121
  protected _rollbackTransaction(state: unknown): Promise<void>;
@@ -156,7 +166,26 @@ declare class PostgresAdapter extends BaseDbAdapter {
156
166
  prepareTypeMapper(): Promise<void>;
157
167
  /** Whether the table declares any `db.geoPoint` fields (unencrypted). */
158
168
  private _hasGeoPointFields;
159
- ensureTable(): Promise<void>;
169
+ ensureTable(opts?: TEnsureTableOptions): Promise<void>;
170
+ /** `schema.name` when the adapter targets a named schema. */
171
+ private _qualify;
172
+ hasRows(tableName?: string): Promise<boolean>;
173
+ /**
174
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
175
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
176
+ * `key_column_usage` ordinal join).
177
+ */
178
+ getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
179
+ getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
180
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
181
+ dropTablesByName(tableNames: string[]): Promise<void>;
182
+ /**
183
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
184
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
185
+ * key columns NOT NULL. A demoted identity column is handled by the
186
+ * `defaultChanged` diff (`DROP IDENTITY`).
187
+ */
188
+ rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
160
189
  private _ensureView;
161
190
  getExistingColumns(): Promise<TExistingColumn[]>;
162
191
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
@@ -171,6 +200,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
171
200
  */
172
201
  private _resetIdentitySequences;
173
202
  tableExists(): Promise<boolean>;
203
+ /**
204
+ * No CASCADE: when something outside the sync inventory (a user view, an
205
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
206
+ * sync reports that as an error entry instead of silently dropping the
207
+ * dependents.
208
+ */
174
209
  dropTable(): Promise<void>;
175
210
  dropColumns(columns: string[]): Promise<void>;
176
211
  dropIndexesForColumns(columns: string[]): Promise<void>;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
2
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance } from "@atscript/db-sql-tools";
1
+ import { BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
2
+ import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor } from "@atscript/db-sql-tools";
3
3
  //#region src/sql-builder.ts
4
4
  /**
5
5
  * PostgreSQL-aware default value for a given design type.
@@ -240,6 +240,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
240
240
  colDefs.push(`PRIMARY KEY (${pkCols})`);
241
241
  }
242
242
  if (foreignKeys) for (const fk of foreignKeys.values()) {
243
+ if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
243
244
  const localCols = fk.fields.map((f) => qi(f)).join(", ");
244
245
  const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
245
246
  let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
@@ -320,14 +321,28 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
320
321
  _vectorFields = /* @__PURE__ */ new Map();
321
322
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
322
323
  _vectorThresholds = /* @__PURE__ */ new Map();
323
- /** Schema name for queries (null falls through to 'public'). */
324
+ /**
325
+ * Schema name for catalog queries and qualification — `@db.schema` of the
326
+ * bound table, or `null` (→ `'public'`, the same default the bound path
327
+ * uses) when the table declares none or the adapter is an administrative
328
+ * one with no readable (the name-taking schema-sync primitives run on such
329
+ * an adapter).
330
+ */
324
331
  get _schema() {
325
- return this._table.schema ?? null;
332
+ return this._table?.schema ?? null;
326
333
  }
327
334
  constructor(driver) {
328
335
  super();
329
336
  this.driver = driver;
330
337
  }
338
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
339
+ _transactionOwner() {
340
+ return this.driver;
341
+ }
342
+ /** The dedicated connection of this pool's open transaction, if any. */
343
+ _txConnection() {
344
+ return this._getTransactionState();
345
+ }
331
346
  async _beginTransaction() {
332
347
  const conn = await this.driver.getConnection();
333
348
  try {
@@ -362,7 +377,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
362
377
  * otherwise the pool-based driver.
363
378
  */
364
379
  _exec() {
365
- return this._getTransactionState() ?? this.driver;
380
+ return this._txConnection() ?? this.driver;
366
381
  }
367
382
  /** PostgreSQL enforces FK constraints natively. */
368
383
  supportsNativeForeignKeys() {
@@ -596,7 +611,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
596
611
  };
597
612
  }
598
613
  async replaceOne(filter, data, expectedVersion) {
599
- return this.updateOne(filter, data, void 0, expectedVersion);
614
+ const full = fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), this._table.versionColumn);
615
+ return this.updateOne(filter, full, void 0, expectedVersion);
600
616
  }
601
617
  async replaceMany(filter, data) {
602
618
  return this.updateMany(filter, data);
@@ -625,7 +641,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
625
641
  _hasGeoPointFields() {
626
642
  return this._table.fieldDescriptors.some((fd) => fd.isGeoPoint && !fd.encrypted);
627
643
  }
628
- async ensureTable() {
644
+ async ensureTable(opts) {
629
645
  if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
630
646
  await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
631
647
  this._citextProvisioned = true;
@@ -635,15 +651,89 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
635
651
  }
636
652
  await this.prepareTypeMapper();
637
653
  if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
638
- if (this._table instanceof AtscriptDbView) return this._ensureView();
654
+ if (this._table.isView) return this._ensureView();
639
655
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
640
656
  incrementFields: this._incrementFields,
641
657
  autoIncrementStart: this._autoIncrementStart,
642
- typeMapper: (field) => this.typeMapper(field)
658
+ typeMapper: (field) => this.typeMapper(field),
659
+ deferForeignKeysTo: opts?.deferForeignKeysTo
643
660
  });
644
661
  this._log(sql);
645
662
  await this._exec().exec(sql);
646
663
  }
664
+ /** `schema.name` when the adapter targets a named schema. */
665
+ _qualify(name) {
666
+ return quoteTableName(this._schema ? `${this._schema}.${name}` : name);
667
+ }
668
+ async hasRows(tableName) {
669
+ const sql = `SELECT EXISTS (SELECT 1 FROM ${tableName ? this._qualify(tableName) : quoteTableName(this.resolveTableName())}) AS "present"`;
670
+ this._log(sql);
671
+ return (await this._exec().get(sql, []))?.present ?? false;
672
+ }
673
+ /**
674
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
675
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
676
+ * `key_column_usage` ordinal join).
677
+ */
678
+ async getReferencingForeignKeys(tableName) {
679
+ return (await this._exec().all(`SELECT cl.relname AS table_name, c.conname AS constraint_name,
680
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
681
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
682
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS columns,
683
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
684
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
685
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_columns
686
+ FROM pg_constraint c
687
+ JOIN pg_class cl ON cl.oid = c.conrelid
688
+ JOIN pg_class rcl ON rcl.oid = c.confrelid
689
+ JOIN pg_namespace rn ON rn.oid = rcl.relnamespace
690
+ WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, 'public')
691
+ ORDER BY cl.relname, c.conname`, [tableName, this._schema])).map((r) => ({
692
+ table: r.table_name,
693
+ fields: r.columns ?? [],
694
+ targetFields: r.ref_columns ?? []
695
+ }));
696
+ }
697
+ async getObjectKind(name) {
698
+ switch ((await this._exec().get(`SELECT c.relkind FROM pg_class c
699
+ JOIN pg_namespace n ON n.oid = c.relnamespace
700
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')
701
+ AND c.relkind IN ('r', 'p', 'v', 'm')`, [name, this._schema]))?.relkind) {
702
+ case "r":
703
+ case "p": return "table";
704
+ case "v": return "view";
705
+ case "m": return "materialized";
706
+ default: return;
707
+ }
708
+ }
709
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
710
+ async dropTablesByName(tableNames) {
711
+ if (tableNames.length === 0) return;
712
+ const ddl = `DROP TABLE IF EXISTS ${tableNames.map((n) => this._qualify(n)).join(", ")}`;
713
+ this._log(ddl);
714
+ await this._exec().exec(ddl);
715
+ }
716
+ /**
717
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
718
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
719
+ * key columns NOT NULL. A demoted identity column is handled by the
720
+ * `defaultChanged` diff (`DROP IDENTITY`).
721
+ */
722
+ async rebuildPrimaryKey(change) {
723
+ const clauses = [];
724
+ if (change.from.length > 0) {
725
+ const row = await this._exec().get(`SELECT c.conname FROM pg_constraint c
726
+ JOIN pg_class cl ON cl.oid = c.conrelid
727
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
728
+ WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [this._table.tableName, this._schema]);
729
+ if (row?.conname) clauses.push(`DROP CONSTRAINT ${qi(row.conname)}`);
730
+ }
731
+ if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
732
+ if (clauses.length === 0) return;
733
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ${clauses.join(", ")}`;
734
+ this._log(ddl);
735
+ await this._exec().exec(ddl);
736
+ }
647
737
  async _ensureView() {
648
738
  const view = this._table;
649
739
  const sql = buildCreateView$1(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref, qi));
@@ -733,15 +823,27 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
733
823
  this._log(ddl);
734
824
  await this._exec().exec(ddl);
735
825
  }
736
- for (const { field } of diff.defaultChanged ?? []) {
737
- let ddl;
738
- if (field.defaultValue?.kind === "value") ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`;
739
- else if (field.defaultValue?.kind === "fn") {
740
- const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : field.defaultValue.fn === "uuid" ? "gen_random_uuid()" : `${field.defaultValue.fn}()`;
741
- ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${fnExpr}`;
742
- } else ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
743
- this._log(ddl);
744
- await this._exec().exec(ddl);
826
+ for (const { field, oldDefault } of diff.defaultChanged ?? []) {
827
+ const col = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)}`;
828
+ const wasIdentity = oldDefault === "fn:increment";
829
+ const statements = [];
830
+ if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") {
831
+ if (!wasIdentity) {
832
+ if (oldDefault !== void 0) statements.push(`${col} DROP DEFAULT`);
833
+ statements.push(`${col} SET NOT NULL`, `${col} ADD GENERATED BY DEFAULT AS IDENTITY`);
834
+ }
835
+ } else {
836
+ if (wasIdentity) statements.push(`${col} DROP IDENTITY IF EXISTS`);
837
+ if (field.defaultValue?.kind === "value") statements.push(`${col} SET DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`);
838
+ else if (field.defaultValue?.kind === "fn") {
839
+ const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : "gen_random_uuid()";
840
+ statements.push(`${col} SET DEFAULT ${fnExpr}`);
841
+ } else if (!wasIdentity) statements.push(`${col} DROP DEFAULT`);
842
+ }
843
+ for (const ddl of statements) {
844
+ this._log(ddl);
845
+ await this._exec().exec(ddl);
846
+ }
745
847
  }
746
848
  return {
747
849
  added,
@@ -866,8 +968,14 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
866
968
  async tableExists() {
867
969
  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;
868
970
  }
971
+ /**
972
+ * No CASCADE: when something outside the sync inventory (a user view, an
973
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
974
+ * sync reports that as an error entry instead of silently dropping the
975
+ * dependents.
976
+ */
869
977
  async dropTable() {
870
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())} CASCADE`;
978
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
871
979
  this._log(ddl);
872
980
  await this._exec().exec(ddl);
873
981
  }
@@ -900,7 +1008,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
900
1008
  }
901
1009
  }
902
1010
  async dropTableByName(tableName) {
903
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
1011
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
904
1012
  this._log(ddl);
905
1013
  await this._exec().exec(ddl);
906
1014
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-postgres",
3
- "version": "0.1.127",
3
+ "version": "0.1.128",
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.89",
47
- "@atscript/typescript": "^0.1.89",
46
+ "@atscript/core": "^0.1.92",
47
+ "@atscript/typescript": "^0.1.92",
48
48
  "@types/pg": "^8.11.0",
49
- "@uniqu/core": "^0.1.7",
49
+ "@uniqu/core": "^0.1.8",
50
50
  "pg": "^8.13.0",
51
- "unplugin-atscript": "^0.1.89"
51
+ "unplugin-atscript": "^0.1.92"
52
52
  },
53
53
  "peerDependencies": {
54
- "@atscript/core": "^0.1.89",
55
- "@atscript/typescript": "^0.1.89",
56
- "@uniqu/core": "^0.1.7",
54
+ "@atscript/core": "^0.1.92",
55
+ "@atscript/typescript": "^0.1.92",
56
+ "@uniqu/core": "^0.1.8",
57
57
  "pg": ">=8.0.0",
58
- "@atscript/db": "^0.1.127",
59
- "@atscript/db-sql-tools": "^0.1.127"
58
+ "@atscript/db-sql-tools": "^0.1.128",
59
+ "@atscript/db": "^0.1.128"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "pg": {