@atscript/db-postgres 0.1.127 → 0.1.129

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})`;
@@ -299,6 +300,40 @@ function parseCount(value) {
299
300
  * const users = space.getTable(UsersType)
300
301
  * ```
301
302
  */
303
+ /** The suffix PostgreSQL gives an auto-named constraint of each `pg_constraint.contype`. */
304
+ const PG_CONSTRAINT_LABELS = {
305
+ p: "pkey",
306
+ u: "key",
307
+ f: "fkey",
308
+ n: "not_null"
309
+ };
310
+ /** UTF-8 byte length of `s`. */
311
+ function byteLength(s) {
312
+ return Buffer.byteLength(s, "utf8");
313
+ }
314
+ /** Cuts `s` to at most `bytes` UTF-8 bytes (whole characters only). */
315
+ function truncateBytes(s, bytes) {
316
+ let out = s;
317
+ while (byteLength(out) > bytes) out = out.slice(0, -1);
318
+ return out;
319
+ }
320
+ /**
321
+ * The name PostgreSQL generates for an unnamed constraint (`makeObjectName`):
322
+ * `<table>_<columns joined by _>_<label>`, with the table and column parts
323
+ * shortened — the longer one first — until the whole fits 63 bytes
324
+ * (`NAMEDATALEN - 1`). Used to recognise and to rename the recreated table's
325
+ * constraints. Ignores the `1`, `2`, … suffix PostgreSQL appends on a clash.
326
+ */
327
+ function pgObjectName(table, columns, label) {
328
+ const name2 = columns.join("_");
329
+ const avail = 63 - (label.length + 1 + (name2 ? 1 : 0));
330
+ let n1 = byteLength(table);
331
+ let n2 = byteLength(name2);
332
+ while (n1 + n2 > avail) if (n1 > n2) n1--;
333
+ else n2--;
334
+ const head = truncateBytes(table, n1);
335
+ return name2 ? `${head}_${truncateBytes(name2, n2)}_${label}` : `${head}_${label}`;
336
+ }
302
337
  var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
303
338
  driver;
304
339
  supportsColumnModify = true;
@@ -321,14 +356,28 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
321
356
  _vectorFields = /* @__PURE__ */ new Map();
322
357
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
323
358
  _vectorThresholds = /* @__PURE__ */ new Map();
324
- /** Schema name for queries (null falls through to 'public'). */
359
+ /**
360
+ * Schema name for catalog queries and qualification — `@db.schema` of the
361
+ * bound table, or `null` (→ `'public'`, the same default the bound path
362
+ * uses) when the table declares none or the adapter is an administrative
363
+ * one with no readable (the name-taking schema-sync primitives run on such
364
+ * an adapter).
365
+ */
325
366
  get _schema() {
326
- return this._table.schema ?? null;
367
+ return this._table?.schema ?? null;
327
368
  }
328
369
  constructor(driver) {
329
370
  super();
330
371
  this.driver = driver;
331
372
  }
373
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
374
+ _transactionOwner() {
375
+ return this.driver;
376
+ }
377
+ /** The dedicated connection of this pool's open transaction, if any. */
378
+ _txConnection() {
379
+ return this._getTransactionState();
380
+ }
332
381
  async _beginTransaction() {
333
382
  const conn = await this.driver.getConnection();
334
383
  try {
@@ -363,7 +412,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
363
412
  * otherwise the pool-based driver.
364
413
  */
365
414
  _exec() {
366
- return this._getTransactionState() ?? this.driver;
415
+ return this._txConnection() ?? this.driver;
367
416
  }
368
417
  /** PostgreSQL enforces FK constraints natively. */
369
418
  supportsNativeForeignKeys() {
@@ -597,7 +646,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
597
646
  };
598
647
  }
599
648
  async replaceOne(filter, data, expectedVersion) {
600
- return this.updateOne(filter, data, void 0, expectedVersion);
649
+ const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), this._table.versionColumn);
650
+ return this.updateOne(filter, full, void 0, expectedVersion);
601
651
  }
602
652
  async replaceMany(filter, data) {
603
653
  return this.updateMany(filter, data);
@@ -626,7 +676,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
626
676
  _hasGeoPointFields() {
627
677
  return this._table.fieldDescriptors.some((fd) => fd.isGeoPoint && !fd.encrypted);
628
678
  }
629
- async ensureTable() {
679
+ async ensureTable(opts) {
630
680
  if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
631
681
  await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
632
682
  this._citextProvisioned = true;
@@ -636,15 +686,89 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
636
686
  }
637
687
  await this.prepareTypeMapper();
638
688
  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();
689
+ if (this._table.isView) return this._ensureView();
640
690
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
641
691
  incrementFields: this._incrementFields,
642
692
  autoIncrementStart: this._autoIncrementStart,
643
- typeMapper: (field) => this.typeMapper(field)
693
+ typeMapper: (field) => this.typeMapper(field),
694
+ deferForeignKeysTo: opts?.deferForeignKeysTo
644
695
  });
645
696
  this._log(sql);
646
697
  await this._exec().exec(sql);
647
698
  }
699
+ /** `schema.name` when the adapter targets a named schema. */
700
+ _qualify(name) {
701
+ return quoteTableName(this._schema ? `${this._schema}.${name}` : name);
702
+ }
703
+ async hasRows(tableName) {
704
+ const sql = `SELECT EXISTS (SELECT 1 FROM ${tableName ? this._qualify(tableName) : quoteTableName(this.resolveTableName())}) AS "present"`;
705
+ this._log(sql);
706
+ return (await this._exec().get(sql, []))?.present ?? false;
707
+ }
708
+ /**
709
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
710
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
711
+ * `key_column_usage` ordinal join).
712
+ */
713
+ async getReferencingForeignKeys(tableName) {
714
+ return (await this._exec().all(`SELECT cl.relname AS table_name, c.conname AS constraint_name,
715
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
716
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
717
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS columns,
718
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
719
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
720
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_columns
721
+ FROM pg_constraint c
722
+ JOIN pg_class cl ON cl.oid = c.conrelid
723
+ JOIN pg_class rcl ON rcl.oid = c.confrelid
724
+ JOIN pg_namespace rn ON rn.oid = rcl.relnamespace
725
+ WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, 'public')
726
+ ORDER BY cl.relname, c.conname`, [tableName, this._schema])).map((r) => ({
727
+ table: r.table_name,
728
+ fields: r.columns ?? [],
729
+ targetFields: r.ref_columns ?? []
730
+ }));
731
+ }
732
+ async getObjectKind(name) {
733
+ switch ((await this._exec().get(`SELECT c.relkind FROM pg_class c
734
+ JOIN pg_namespace n ON n.oid = c.relnamespace
735
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')
736
+ AND c.relkind IN ('r', 'p', 'v', 'm')`, [name, this._schema]))?.relkind) {
737
+ case "r":
738
+ case "p": return "table";
739
+ case "v": return "view";
740
+ case "m": return "materialized";
741
+ default: return;
742
+ }
743
+ }
744
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
745
+ async dropTablesByName(tableNames) {
746
+ if (tableNames.length === 0) return;
747
+ const ddl = `DROP TABLE IF EXISTS ${tableNames.map((n) => this._qualify(n)).join(", ")}`;
748
+ this._log(ddl);
749
+ await this._exec().exec(ddl);
750
+ }
751
+ /**
752
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
753
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
754
+ * key columns NOT NULL. A demoted identity column is handled by the
755
+ * `defaultChanged` diff (`DROP IDENTITY`).
756
+ */
757
+ async rebuildPrimaryKey(change) {
758
+ const clauses = [];
759
+ if (change.from.length > 0) {
760
+ const row = await this._exec().get(`SELECT c.conname FROM pg_constraint c
761
+ JOIN pg_class cl ON cl.oid = c.conrelid
762
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
763
+ WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [this._table.tableName, this._schema]);
764
+ if (row?.conname) clauses.push(`DROP CONSTRAINT ${qi(row.conname)}`);
765
+ }
766
+ if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
767
+ if (clauses.length === 0) return;
768
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ${clauses.join(", ")}`;
769
+ this._log(ddl);
770
+ await this._exec().exec(ddl);
771
+ }
648
772
  async _ensureView() {
649
773
  const view = this._table;
650
774
  const sql = buildCreateView(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref, qi));
@@ -734,15 +858,27 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
734
858
  this._log(ddl);
735
859
  await this._exec().exec(ddl);
736
860
  }
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);
861
+ for (const { field, oldDefault } of diff.defaultChanged ?? []) {
862
+ const col = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)}`;
863
+ const wasIdentity = oldDefault === "fn:increment";
864
+ const statements = [];
865
+ if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") {
866
+ if (!wasIdentity) {
867
+ if (oldDefault !== void 0) statements.push(`${col} DROP DEFAULT`);
868
+ statements.push(`${col} SET NOT NULL`, `${col} ADD GENERATED BY DEFAULT AS IDENTITY`);
869
+ }
870
+ } else {
871
+ if (wasIdentity) statements.push(`${col} DROP IDENTITY IF EXISTS`);
872
+ if (field.defaultValue?.kind === "value") statements.push(`${col} SET DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`);
873
+ else if (field.defaultValue?.kind === "fn") {
874
+ const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : "gen_random_uuid()";
875
+ statements.push(`${col} SET DEFAULT ${fnExpr}`);
876
+ } else if (!wasIdentity) statements.push(`${col} DROP DEFAULT`);
877
+ }
878
+ for (const ddl of statements) {
879
+ this._log(ddl);
880
+ await this._exec().exec(ddl);
881
+ }
746
882
  }
747
883
  return {
748
884
  added,
@@ -767,14 +903,18 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
767
903
  ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
768
904
  JOIN information_schema.key_column_usage kcur
769
905
  ON kcur.constraint_name = rc.unique_constraint_name AND kcur.table_schema = rc.unique_constraint_schema
770
- AND kcur.ordinal_position = kcu.ordinal_position
906
+ AND kcur.ordinal_position = kcu.position_in_unique_constraint
771
907
  WHERE rc.unique_constraint_schema = COALESCE($1, 'public')
772
908
  AND rc.unique_constraint_name IN (
773
909
  SELECT constraint_name FROM information_schema.table_constraints
774
- WHERE table_name = $2 AND table_schema = COALESCE($1, 'public') AND constraint_type = 'PRIMARY KEY'
775
- )`, [schema, this._table.tableName]);
910
+ WHERE table_name = $2 AND table_schema = COALESCE($1, 'public')
911
+ AND constraint_type IN ('PRIMARY KEY', 'UNIQUE')
912
+ )
913
+ ORDER BY tc.table_schema, tc.table_name, tc.constraint_name, kcu.ordinal_position`, [schema, this._table.tableName]);
776
914
  const fkByName = /* @__PURE__ */ new Map();
915
+ const ownSchema = schema ?? "public";
777
916
  for (const fk of fkRefs) {
917
+ if (fk.table_schema === ownSchema && fk.table_name === this._table.tableName) continue;
778
918
  let entry = fkByName.get(fk.constraint_name);
779
919
  if (!entry) {
780
920
  entry = {
@@ -798,7 +938,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
798
938
  const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, {
799
939
  incrementFields: this._incrementFields,
800
940
  autoIncrementStart: this._autoIncrementStart,
801
- typeMapper: (field) => this.typeMapper(field)
941
+ typeMapper: (field) => this.typeMapper(field),
942
+ deferForeignKeysTo: new Set([this._table.tableName])
802
943
  });
803
944
  this._log(createSql);
804
945
  await conn.exec(createSql);
@@ -823,13 +964,16 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
823
964
  this._log(copySql);
824
965
  await conn.exec(copySql);
825
966
  }
826
- await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`);
967
+ const dropSql = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
968
+ this._log(dropSql);
969
+ await conn.exec(dropSql);
827
970
  await conn.exec(`ALTER TABLE ${quoteTableName(tempName)} RENAME TO ${qi(this._table.tableName)}`);
971
+ await this._renameTempConstraints(conn, baseTempName);
828
972
  const resolvedTable = this.resolveTableName();
829
- for (const [, fk] of fkByName) {
973
+ for (const [name, fk] of fkByName) {
830
974
  const localCols = fk.cols.map((c) => qi(c)).join(", ");
831
975
  const refCols = fk.refCols.map((c) => qi(c)).join(", ");
832
- let ddl = `ALTER TABLE ${qi(fk.schema)}.${qi(fk.table)} ADD FOREIGN KEY (${localCols}) REFERENCES ${quoteTableName(resolvedTable)} (${refCols})`;
976
+ let ddl = `ALTER TABLE ${qi(fk.schema)}.${qi(fk.table)} ADD CONSTRAINT ${qi(name)} FOREIGN KEY (${localCols}) REFERENCES ${quoteTableName(resolvedTable)} (${refCols})`;
833
977
  if (fk.onDelete !== "NO ACTION") ddl += ` ON DELETE ${fk.onDelete}`;
834
978
  if (fk.onUpdate !== "NO ACTION") ddl += ` ON UPDATE ${fk.onUpdate}`;
835
979
  this._log(ddl);
@@ -839,11 +983,59 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
839
983
  await this._resetIdentitySequences();
840
984
  } catch (err) {
841
985
  await conn.exec("ROLLBACK").catch(() => {});
986
+ const detail = err.detail;
987
+ if (err instanceof Error && typeof detail === "string" && detail.length > 0) err.message = `${err.message} — ${detail}`;
842
988
  throw err;
843
989
  } finally {
844
990
  conn.release();
845
991
  }
846
992
  }
993
+ /**
994
+ * Renames the recreated table's auto-named constraints from the temp
995
+ * table's names to the final table's: `<tmp>_pkey` → `<table>_pkey`,
996
+ * `<tmp>_<cols>_fkey` → `<table>_<cols>_fkey`, and so on. Runs inside the
997
+ * recreate transaction, after the RENAME. A constraint is recognised by
998
+ * its kind and columns — its name equals the one PostgreSQL generates for
999
+ * the temp table ({@link pgObjectName}, 63-byte truncation included) — so
1000
+ * a long table name matches too. A target name already taken (a primary /
1001
+ * unique constraint's index shares its name with every relation in the
1002
+ * schema; other kinds with the table's constraints) is skipped and logged.
1003
+ */
1004
+ async _renameTempConstraints(conn, baseTempName) {
1005
+ const table = this._table.tableName;
1006
+ const rows = await conn.all(`SELECT c.conname, c.contype,
1007
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
1008
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1009
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS columns
1010
+ FROM pg_constraint c
1011
+ JOIN pg_class cl ON cl.oid = c.conrelid
1012
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
1013
+ WHERE cl.relname = $1 AND n.nspname = COALESCE($2, 'public')
1014
+ ORDER BY c.conname`, [table, this._schema]);
1015
+ const taken = new Set(rows.map((r) => r.conname));
1016
+ for (const { conname, contype, columns } of rows) {
1017
+ const label = PG_CONSTRAINT_LABELS[contype];
1018
+ if (!label) continue;
1019
+ const cols = contype === "p" ? [] : columns ?? [];
1020
+ if (conname !== pgObjectName(baseTempName, cols, label)) continue;
1021
+ const target = pgObjectName(table, cols, label);
1022
+ if (target === conname) continue;
1023
+ if (contype === "p" || contype === "u" ? await this._relationExists(conn, target) : taken.has(target)) {
1024
+ this._log(`-- constraint "${conname}" keeps its name: "${target}" already exists`);
1025
+ continue;
1026
+ }
1027
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} RENAME CONSTRAINT ${qi(conname)} TO ${qi(target)}`;
1028
+ this._log(ddl);
1029
+ await conn.exec(ddl);
1030
+ taken.add(target);
1031
+ }
1032
+ }
1033
+ /** Whether a relation (table, index, view, …) named `name` exists in this adapter's schema. */
1034
+ async _relationExists(conn, name) {
1035
+ return (await conn.get(`SELECT true AS present FROM pg_class c
1036
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1037
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [name, this._schema]))?.present ?? false;
1038
+ }
847
1039
  async afterSyncTable() {
848
1040
  await this._resetIdentitySequences();
849
1041
  }
@@ -867,8 +1059,14 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
867
1059
  async tableExists() {
868
1060
  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
1061
  }
1062
+ /**
1063
+ * No CASCADE: when something outside the sync inventory (a user view, an
1064
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
1065
+ * sync reports that as an error entry instead of silently dropping the
1066
+ * dependents.
1067
+ */
870
1068
  async dropTable() {
871
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())} CASCADE`;
1069
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
872
1070
  this._log(ddl);
873
1071
  await this._exec().exec(ddl);
874
1072
  }
@@ -901,7 +1099,7 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
901
1099
  }
902
1100
  }
903
1101
  async dropTableByName(tableName) {
904
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
1102
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
905
1103
  this._log(ddl);
906
1104
  await this._exec().exec(ddl);
907
1105
  }
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
 
@@ -69,22 +69,6 @@ interface TPgConnection {
69
69
  }
70
70
  //#endregion
71
71
  //#region src/postgres-adapter.d.ts
72
- /**
73
- * PostgreSQL adapter for {@link AtscriptDbTable}.
74
- *
75
- * Accepts any {@link TPgDriver} implementation — the actual PostgreSQL driver
76
- * is fully swappable (pg Pool, custom implementations, etc.).
77
- *
78
- * Usage:
79
- * ```typescript
80
- * import { PgDriver, PostgresAdapter } from '@atscript/db-postgres'
81
- * import { DbSpace } from '@atscript/db'
82
- *
83
- * const driver = new PgDriver('postgresql://user@localhost:5432/mydb')
84
- * const space = new DbSpace(() => new PostgresAdapter(driver))
85
- * const users = space.getTable(UsersType)
86
- * ```
87
- */
88
72
  declare class PostgresAdapter extends BaseDbAdapter {
89
73
  protected readonly driver: TPgDriver;
90
74
  supportsColumnModify: boolean;
@@ -103,9 +87,19 @@ declare class PostgresAdapter extends BaseDbAdapter {
103
87
  private _vectorFields;
104
88
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
105
89
  private _vectorThresholds;
106
- /** Schema name for queries (null falls through to 'public'). */
90
+ /**
91
+ * Schema name for catalog queries and qualification — `@db.schema` of the
92
+ * bound table, or `null` (→ `'public'`, the same default the bound path
93
+ * uses) when the table declares none or the adapter is an administrative
94
+ * one with no readable (the name-taking schema-sync primitives run on such
95
+ * an adapter).
96
+ */
107
97
  private get _schema();
108
98
  constructor(driver: TPgDriver);
99
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
100
+ protected _transactionOwner(): unknown;
101
+ /** The dedicated connection of this pool's open transaction, if any. */
102
+ private _txConnection;
109
103
  protected _beginTransaction(): Promise<TPgConnection>;
110
104
  protected _commitTransaction(state: unknown): Promise<void>;
111
105
  protected _rollbackTransaction(state: unknown): Promise<void>;
@@ -156,12 +150,45 @@ declare class PostgresAdapter extends BaseDbAdapter {
156
150
  prepareTypeMapper(): Promise<void>;
157
151
  /** Whether the table declares any `db.geoPoint` fields (unencrypted). */
158
152
  private _hasGeoPointFields;
159
- ensureTable(): Promise<void>;
153
+ ensureTable(opts?: TEnsureTableOptions): Promise<void>;
154
+ /** `schema.name` when the adapter targets a named schema. */
155
+ private _qualify;
156
+ hasRows(tableName?: string): Promise<boolean>;
157
+ /**
158
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
159
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
160
+ * `key_column_usage` ordinal join).
161
+ */
162
+ getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
163
+ getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
164
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
165
+ dropTablesByName(tableNames: string[]): Promise<void>;
166
+ /**
167
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
168
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
169
+ * key columns NOT NULL. A demoted identity column is handled by the
170
+ * `defaultChanged` diff (`DROP IDENTITY`).
171
+ */
172
+ rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
160
173
  private _ensureView;
161
174
  getExistingColumns(): Promise<TExistingColumn[]>;
162
175
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
163
176
  syncColumns(diff: TColumnDiff): Promise<TSyncColumnResult>;
164
177
  recreateTable(): Promise<void>;
178
+ /**
179
+ * Renames the recreated table's auto-named constraints from the temp
180
+ * table's names to the final table's: `<tmp>_pkey` → `<table>_pkey`,
181
+ * `<tmp>_<cols>_fkey` → `<table>_<cols>_fkey`, and so on. Runs inside the
182
+ * recreate transaction, after the RENAME. A constraint is recognised by
183
+ * its kind and columns — its name equals the one PostgreSQL generates for
184
+ * the temp table ({@link pgObjectName}, 63-byte truncation included) — so
185
+ * a long table name matches too. A target name already taken (a primary /
186
+ * unique constraint's index shares its name with every relation in the
187
+ * schema; other kinds with the table's constraints) is skipped and logged.
188
+ */
189
+ private _renameTempConstraints;
190
+ /** Whether a relation (table, index, view, …) named `name` exists in this adapter's schema. */
191
+ private _relationExists;
165
192
  afterSyncTable(): Promise<void>;
166
193
  /**
167
194
  * Resets IDENTITY sequences to MAX(column) so that the next auto-generated
@@ -171,6 +198,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
171
198
  */
172
199
  private _resetIdentitySequences;
173
200
  tableExists(): Promise<boolean>;
201
+ /**
202
+ * No CASCADE: when something outside the sync inventory (a user view, an
203
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
204
+ * sync reports that as an error entry instead of silently dropping the
205
+ * dependents.
206
+ */
174
207
  dropTable(): Promise<void>;
175
208
  dropColumns(columns: string[]): Promise<void>;
176
209
  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
 
@@ -69,22 +69,6 @@ interface TPgConnection {
69
69
  }
70
70
  //#endregion
71
71
  //#region src/postgres-adapter.d.ts
72
- /**
73
- * PostgreSQL adapter for {@link AtscriptDbTable}.
74
- *
75
- * Accepts any {@link TPgDriver} implementation — the actual PostgreSQL driver
76
- * is fully swappable (pg Pool, custom implementations, etc.).
77
- *
78
- * Usage:
79
- * ```typescript
80
- * import { PgDriver, PostgresAdapter } from '@atscript/db-postgres'
81
- * import { DbSpace } from '@atscript/db'
82
- *
83
- * const driver = new PgDriver('postgresql://user@localhost:5432/mydb')
84
- * const space = new DbSpace(() => new PostgresAdapter(driver))
85
- * const users = space.getTable(UsersType)
86
- * ```
87
- */
88
72
  declare class PostgresAdapter extends BaseDbAdapter {
89
73
  protected readonly driver: TPgDriver;
90
74
  supportsColumnModify: boolean;
@@ -103,9 +87,19 @@ declare class PostgresAdapter extends BaseDbAdapter {
103
87
  private _vectorFields;
104
88
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
105
89
  private _vectorThresholds;
106
- /** Schema name for queries (null falls through to 'public'). */
90
+ /**
91
+ * Schema name for catalog queries and qualification — `@db.schema` of the
92
+ * bound table, or `null` (→ `'public'`, the same default the bound path
93
+ * uses) when the table declares none or the adapter is an administrative
94
+ * one with no readable (the name-taking schema-sync primitives run on such
95
+ * an adapter).
96
+ */
107
97
  private get _schema();
108
98
  constructor(driver: TPgDriver);
99
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
100
+ protected _transactionOwner(): unknown;
101
+ /** The dedicated connection of this pool's open transaction, if any. */
102
+ private _txConnection;
109
103
  protected _beginTransaction(): Promise<TPgConnection>;
110
104
  protected _commitTransaction(state: unknown): Promise<void>;
111
105
  protected _rollbackTransaction(state: unknown): Promise<void>;
@@ -156,12 +150,45 @@ declare class PostgresAdapter extends BaseDbAdapter {
156
150
  prepareTypeMapper(): Promise<void>;
157
151
  /** Whether the table declares any `db.geoPoint` fields (unencrypted). */
158
152
  private _hasGeoPointFields;
159
- ensureTable(): Promise<void>;
153
+ ensureTable(opts?: TEnsureTableOptions): Promise<void>;
154
+ /** `schema.name` when the adapter targets a named schema. */
155
+ private _qualify;
156
+ hasRows(tableName?: string): Promise<boolean>;
157
+ /**
158
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
159
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
160
+ * `key_column_usage` ordinal join).
161
+ */
162
+ getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
163
+ getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
164
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
165
+ dropTablesByName(tableNames: string[]): Promise<void>;
166
+ /**
167
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
168
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
169
+ * key columns NOT NULL. A demoted identity column is handled by the
170
+ * `defaultChanged` diff (`DROP IDENTITY`).
171
+ */
172
+ rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
160
173
  private _ensureView;
161
174
  getExistingColumns(): Promise<TExistingColumn[]>;
162
175
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
163
176
  syncColumns(diff: TColumnDiff): Promise<TSyncColumnResult>;
164
177
  recreateTable(): Promise<void>;
178
+ /**
179
+ * Renames the recreated table's auto-named constraints from the temp
180
+ * table's names to the final table's: `<tmp>_pkey` → `<table>_pkey`,
181
+ * `<tmp>_<cols>_fkey` → `<table>_<cols>_fkey`, and so on. Runs inside the
182
+ * recreate transaction, after the RENAME. A constraint is recognised by
183
+ * its kind and columns — its name equals the one PostgreSQL generates for
184
+ * the temp table ({@link pgObjectName}, 63-byte truncation included) — so
185
+ * a long table name matches too. A target name already taken (a primary /
186
+ * unique constraint's index shares its name with every relation in the
187
+ * schema; other kinds with the table's constraints) is skipped and logged.
188
+ */
189
+ private _renameTempConstraints;
190
+ /** Whether a relation (table, index, view, …) named `name` exists in this adapter's schema. */
191
+ private _relationExists;
165
192
  afterSyncTable(): Promise<void>;
166
193
  /**
167
194
  * Resets IDENTITY sequences to MAX(column) so that the next auto-generated
@@ -171,6 +198,12 @@ declare class PostgresAdapter extends BaseDbAdapter {
171
198
  */
172
199
  private _resetIdentitySequences;
173
200
  tableExists(): Promise<boolean>;
201
+ /**
202
+ * No CASCADE: when something outside the sync inventory (a user view, an
203
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
204
+ * sync reports that as an error entry instead of silently dropping the
205
+ * dependents.
206
+ */
174
207
  dropTable(): Promise<void>;
175
208
  dropColumns(columns: string[]): Promise<void>;
176
209
  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})`;
@@ -298,6 +299,40 @@ function parseCount(value) {
298
299
  * const users = space.getTable(UsersType)
299
300
  * ```
300
301
  */
302
+ /** The suffix PostgreSQL gives an auto-named constraint of each `pg_constraint.contype`. */
303
+ const PG_CONSTRAINT_LABELS = {
304
+ p: "pkey",
305
+ u: "key",
306
+ f: "fkey",
307
+ n: "not_null"
308
+ };
309
+ /** UTF-8 byte length of `s`. */
310
+ function byteLength(s) {
311
+ return Buffer.byteLength(s, "utf8");
312
+ }
313
+ /** Cuts `s` to at most `bytes` UTF-8 bytes (whole characters only). */
314
+ function truncateBytes(s, bytes) {
315
+ let out = s;
316
+ while (byteLength(out) > bytes) out = out.slice(0, -1);
317
+ return out;
318
+ }
319
+ /**
320
+ * The name PostgreSQL generates for an unnamed constraint (`makeObjectName`):
321
+ * `<table>_<columns joined by _>_<label>`, with the table and column parts
322
+ * shortened — the longer one first — until the whole fits 63 bytes
323
+ * (`NAMEDATALEN - 1`). Used to recognise and to rename the recreated table's
324
+ * constraints. Ignores the `1`, `2`, … suffix PostgreSQL appends on a clash.
325
+ */
326
+ function pgObjectName(table, columns, label) {
327
+ const name2 = columns.join("_");
328
+ const avail = 63 - (label.length + 1 + (name2 ? 1 : 0));
329
+ let n1 = byteLength(table);
330
+ let n2 = byteLength(name2);
331
+ while (n1 + n2 > avail) if (n1 > n2) n1--;
332
+ else n2--;
333
+ const head = truncateBytes(table, n1);
334
+ return name2 ? `${head}_${truncateBytes(name2, n2)}_${label}` : `${head}_${label}`;
335
+ }
301
336
  var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
302
337
  driver;
303
338
  supportsColumnModify = true;
@@ -320,14 +355,28 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
320
355
  _vectorFields = /* @__PURE__ */ new Map();
321
356
  /** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
322
357
  _vectorThresholds = /* @__PURE__ */ new Map();
323
- /** Schema name for queries (null falls through to 'public'). */
358
+ /**
359
+ * Schema name for catalog queries and qualification — `@db.schema` of the
360
+ * bound table, or `null` (→ `'public'`, the same default the bound path
361
+ * uses) when the table declares none or the adapter is an administrative
362
+ * one with no readable (the name-taking schema-sync primitives run on such
363
+ * an adapter).
364
+ */
324
365
  get _schema() {
325
- return this._table.schema ?? null;
366
+ return this._table?.schema ?? null;
326
367
  }
327
368
  constructor(driver) {
328
369
  super();
329
370
  this.driver = driver;
330
371
  }
372
+ /** Every adapter over this pool shares one transaction (since 0.1.128). */
373
+ _transactionOwner() {
374
+ return this.driver;
375
+ }
376
+ /** The dedicated connection of this pool's open transaction, if any. */
377
+ _txConnection() {
378
+ return this._getTransactionState();
379
+ }
331
380
  async _beginTransaction() {
332
381
  const conn = await this.driver.getConnection();
333
382
  try {
@@ -362,7 +411,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
362
411
  * otherwise the pool-based driver.
363
412
  */
364
413
  _exec() {
365
- return this._getTransactionState() ?? this.driver;
414
+ return this._txConnection() ?? this.driver;
366
415
  }
367
416
  /** PostgreSQL enforces FK constraints natively. */
368
417
  supportsNativeForeignKeys() {
@@ -596,7 +645,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
596
645
  };
597
646
  }
598
647
  async replaceOne(filter, data, expectedVersion) {
599
- return this.updateOne(filter, data, void 0, expectedVersion);
648
+ const full = fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), this._table.versionColumn);
649
+ return this.updateOne(filter, full, void 0, expectedVersion);
600
650
  }
601
651
  async replaceMany(filter, data) {
602
652
  return this.updateMany(filter, data);
@@ -625,7 +675,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
625
675
  _hasGeoPointFields() {
626
676
  return this._table.fieldDescriptors.some((fd) => fd.isGeoPoint && !fd.encrypted);
627
677
  }
628
- async ensureTable() {
678
+ async ensureTable(opts) {
629
679
  if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
630
680
  await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
631
681
  this._citextProvisioned = true;
@@ -635,15 +685,89 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
635
685
  }
636
686
  await this.prepareTypeMapper();
637
687
  if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
638
- if (this._table instanceof AtscriptDbView) return this._ensureView();
688
+ if (this._table.isView) return this._ensureView();
639
689
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
640
690
  incrementFields: this._incrementFields,
641
691
  autoIncrementStart: this._autoIncrementStart,
642
- typeMapper: (field) => this.typeMapper(field)
692
+ typeMapper: (field) => this.typeMapper(field),
693
+ deferForeignKeysTo: opts?.deferForeignKeysTo
643
694
  });
644
695
  this._log(sql);
645
696
  await this._exec().exec(sql);
646
697
  }
698
+ /** `schema.name` when the adapter targets a named schema. */
699
+ _qualify(name) {
700
+ return quoteTableName(this._schema ? `${this._schema}.${name}` : name);
701
+ }
702
+ async hasRows(tableName) {
703
+ const sql = `SELECT EXISTS (SELECT 1 FROM ${tableName ? this._qualify(tableName) : quoteTableName(this.resolveTableName())}) AS "present"`;
704
+ this._log(sql);
705
+ return (await this._exec().get(sql, []))?.present ?? false;
706
+ }
707
+ /**
708
+ * Live foreign keys referencing `tableName`, via `pg_constraint` — exact for
709
+ * composite keys (`conkey`/`confkey` are positionally aligned, unlike the
710
+ * `key_column_usage` ordinal join).
711
+ */
712
+ async getReferencingForeignKeys(tableName) {
713
+ return (await this._exec().all(`SELECT cl.relname AS table_name, c.conname AS constraint_name,
714
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
715
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
716
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS columns,
717
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
718
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
719
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_columns
720
+ FROM pg_constraint c
721
+ JOIN pg_class cl ON cl.oid = c.conrelid
722
+ JOIN pg_class rcl ON rcl.oid = c.confrelid
723
+ JOIN pg_namespace rn ON rn.oid = rcl.relnamespace
724
+ WHERE c.contype = 'f' AND rcl.relname = $1 AND rn.nspname = COALESCE($2, 'public')
725
+ ORDER BY cl.relname, c.conname`, [tableName, this._schema])).map((r) => ({
726
+ table: r.table_name,
727
+ fields: r.columns ?? [],
728
+ targetFields: r.ref_columns ?? []
729
+ }));
730
+ }
731
+ async getObjectKind(name) {
732
+ switch ((await this._exec().get(`SELECT c.relkind FROM pg_class c
733
+ JOIN pg_namespace n ON n.oid = c.relnamespace
734
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')
735
+ AND c.relkind IN ('r', 'p', 'v', 'm')`, [name, this._schema]))?.relkind) {
736
+ case "r":
737
+ case "p": return "table";
738
+ case "v": return "view";
739
+ case "m": return "materialized";
740
+ default: return;
741
+ }
742
+ }
743
+ /** One multi-table `DROP TABLE a, b` — PostgreSQL resolves the mutual FKs inside the set. No CASCADE. */
744
+ async dropTablesByName(tableNames) {
745
+ if (tableNames.length === 0) return;
746
+ const ddl = `DROP TABLE IF EXISTS ${tableNames.map((n) => this._qualify(n)).join(", ")}`;
747
+ this._log(ddl);
748
+ await this._exec().exec(ddl);
749
+ }
750
+ /**
751
+ * `ALTER TABLE … DROP CONSTRAINT <pk>, ADD PRIMARY KEY (…)` in ONE statement
752
+ * (atomic). Called on an empty table only; `ADD PRIMARY KEY` makes the new
753
+ * key columns NOT NULL. A demoted identity column is handled by the
754
+ * `defaultChanged` diff (`DROP IDENTITY`).
755
+ */
756
+ async rebuildPrimaryKey(change) {
757
+ const clauses = [];
758
+ if (change.from.length > 0) {
759
+ const row = await this._exec().get(`SELECT c.conname FROM pg_constraint c
760
+ JOIN pg_class cl ON cl.oid = c.conrelid
761
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
762
+ WHERE c.contype = 'p' AND cl.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [this._table.tableName, this._schema]);
763
+ if (row?.conname) clauses.push(`DROP CONSTRAINT ${qi(row.conname)}`);
764
+ }
765
+ if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
766
+ if (clauses.length === 0) return;
767
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ${clauses.join(", ")}`;
768
+ this._log(ddl);
769
+ await this._exec().exec(ddl);
770
+ }
647
771
  async _ensureView() {
648
772
  const view = this._table;
649
773
  const sql = buildCreateView$1(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref, qi));
@@ -733,15 +857,27 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
733
857
  this._log(ddl);
734
858
  await this._exec().exec(ddl);
735
859
  }
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);
860
+ for (const { field, oldDefault } of diff.defaultChanged ?? []) {
861
+ const col = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)}`;
862
+ const wasIdentity = oldDefault === "fn:increment";
863
+ const statements = [];
864
+ if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") {
865
+ if (!wasIdentity) {
866
+ if (oldDefault !== void 0) statements.push(`${col} DROP DEFAULT`);
867
+ statements.push(`${col} SET NOT NULL`, `${col} ADD GENERATED BY DEFAULT AS IDENTITY`);
868
+ }
869
+ } else {
870
+ if (wasIdentity) statements.push(`${col} DROP IDENTITY IF EXISTS`);
871
+ if (field.defaultValue?.kind === "value") statements.push(`${col} SET DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`);
872
+ else if (field.defaultValue?.kind === "fn") {
873
+ const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : "gen_random_uuid()";
874
+ statements.push(`${col} SET DEFAULT ${fnExpr}`);
875
+ } else if (!wasIdentity) statements.push(`${col} DROP DEFAULT`);
876
+ }
877
+ for (const ddl of statements) {
878
+ this._log(ddl);
879
+ await this._exec().exec(ddl);
880
+ }
745
881
  }
746
882
  return {
747
883
  added,
@@ -766,14 +902,18 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
766
902
  ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
767
903
  JOIN information_schema.key_column_usage kcur
768
904
  ON kcur.constraint_name = rc.unique_constraint_name AND kcur.table_schema = rc.unique_constraint_schema
769
- AND kcur.ordinal_position = kcu.ordinal_position
905
+ AND kcur.ordinal_position = kcu.position_in_unique_constraint
770
906
  WHERE rc.unique_constraint_schema = COALESCE($1, 'public')
771
907
  AND rc.unique_constraint_name IN (
772
908
  SELECT constraint_name FROM information_schema.table_constraints
773
- WHERE table_name = $2 AND table_schema = COALESCE($1, 'public') AND constraint_type = 'PRIMARY KEY'
774
- )`, [schema, this._table.tableName]);
909
+ WHERE table_name = $2 AND table_schema = COALESCE($1, 'public')
910
+ AND constraint_type IN ('PRIMARY KEY', 'UNIQUE')
911
+ )
912
+ ORDER BY tc.table_schema, tc.table_name, tc.constraint_name, kcu.ordinal_position`, [schema, this._table.tableName]);
775
913
  const fkByName = /* @__PURE__ */ new Map();
914
+ const ownSchema = schema ?? "public";
776
915
  for (const fk of fkRefs) {
916
+ if (fk.table_schema === ownSchema && fk.table_name === this._table.tableName) continue;
777
917
  let entry = fkByName.get(fk.constraint_name);
778
918
  if (!entry) {
779
919
  entry = {
@@ -797,7 +937,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
797
937
  const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, {
798
938
  incrementFields: this._incrementFields,
799
939
  autoIncrementStart: this._autoIncrementStart,
800
- typeMapper: (field) => this.typeMapper(field)
940
+ typeMapper: (field) => this.typeMapper(field),
941
+ deferForeignKeysTo: new Set([this._table.tableName])
801
942
  });
802
943
  this._log(createSql);
803
944
  await conn.exec(createSql);
@@ -822,13 +963,16 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
822
963
  this._log(copySql);
823
964
  await conn.exec(copySql);
824
965
  }
825
- await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`);
966
+ const dropSql = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
967
+ this._log(dropSql);
968
+ await conn.exec(dropSql);
826
969
  await conn.exec(`ALTER TABLE ${quoteTableName(tempName)} RENAME TO ${qi(this._table.tableName)}`);
970
+ await this._renameTempConstraints(conn, baseTempName);
827
971
  const resolvedTable = this.resolveTableName();
828
- for (const [, fk] of fkByName) {
972
+ for (const [name, fk] of fkByName) {
829
973
  const localCols = fk.cols.map((c) => qi(c)).join(", ");
830
974
  const refCols = fk.refCols.map((c) => qi(c)).join(", ");
831
- let ddl = `ALTER TABLE ${qi(fk.schema)}.${qi(fk.table)} ADD FOREIGN KEY (${localCols}) REFERENCES ${quoteTableName(resolvedTable)} (${refCols})`;
975
+ let ddl = `ALTER TABLE ${qi(fk.schema)}.${qi(fk.table)} ADD CONSTRAINT ${qi(name)} FOREIGN KEY (${localCols}) REFERENCES ${quoteTableName(resolvedTable)} (${refCols})`;
832
976
  if (fk.onDelete !== "NO ACTION") ddl += ` ON DELETE ${fk.onDelete}`;
833
977
  if (fk.onUpdate !== "NO ACTION") ddl += ` ON UPDATE ${fk.onUpdate}`;
834
978
  this._log(ddl);
@@ -838,11 +982,59 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
838
982
  await this._resetIdentitySequences();
839
983
  } catch (err) {
840
984
  await conn.exec("ROLLBACK").catch(() => {});
985
+ const detail = err.detail;
986
+ if (err instanceof Error && typeof detail === "string" && detail.length > 0) err.message = `${err.message} — ${detail}`;
841
987
  throw err;
842
988
  } finally {
843
989
  conn.release();
844
990
  }
845
991
  }
992
+ /**
993
+ * Renames the recreated table's auto-named constraints from the temp
994
+ * table's names to the final table's: `<tmp>_pkey` → `<table>_pkey`,
995
+ * `<tmp>_<cols>_fkey` → `<table>_<cols>_fkey`, and so on. Runs inside the
996
+ * recreate transaction, after the RENAME. A constraint is recognised by
997
+ * its kind and columns — its name equals the one PostgreSQL generates for
998
+ * the temp table ({@link pgObjectName}, 63-byte truncation included) — so
999
+ * a long table name matches too. A target name already taken (a primary /
1000
+ * unique constraint's index shares its name with every relation in the
1001
+ * schema; other kinds with the table's constraints) is skipped and logged.
1002
+ */
1003
+ async _renameTempConstraints(conn, baseTempName) {
1004
+ const table = this._table.tableName;
1005
+ const rows = await conn.all(`SELECT c.conname, c.contype,
1006
+ (SELECT array_agg(a.attname::text ORDER BY k.ord)
1007
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1008
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS columns
1009
+ FROM pg_constraint c
1010
+ JOIN pg_class cl ON cl.oid = c.conrelid
1011
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
1012
+ WHERE cl.relname = $1 AND n.nspname = COALESCE($2, 'public')
1013
+ ORDER BY c.conname`, [table, this._schema]);
1014
+ const taken = new Set(rows.map((r) => r.conname));
1015
+ for (const { conname, contype, columns } of rows) {
1016
+ const label = PG_CONSTRAINT_LABELS[contype];
1017
+ if (!label) continue;
1018
+ const cols = contype === "p" ? [] : columns ?? [];
1019
+ if (conname !== pgObjectName(baseTempName, cols, label)) continue;
1020
+ const target = pgObjectName(table, cols, label);
1021
+ if (target === conname) continue;
1022
+ if (contype === "p" || contype === "u" ? await this._relationExists(conn, target) : taken.has(target)) {
1023
+ this._log(`-- constraint "${conname}" keeps its name: "${target}" already exists`);
1024
+ continue;
1025
+ }
1026
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} RENAME CONSTRAINT ${qi(conname)} TO ${qi(target)}`;
1027
+ this._log(ddl);
1028
+ await conn.exec(ddl);
1029
+ taken.add(target);
1030
+ }
1031
+ }
1032
+ /** Whether a relation (table, index, view, …) named `name` exists in this adapter's schema. */
1033
+ async _relationExists(conn, name) {
1034
+ return (await conn.get(`SELECT true AS present FROM pg_class c
1035
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1036
+ WHERE c.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [name, this._schema]))?.present ?? false;
1037
+ }
846
1038
  async afterSyncTable() {
847
1039
  await this._resetIdentitySequences();
848
1040
  }
@@ -866,8 +1058,14 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
866
1058
  async tableExists() {
867
1059
  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
1060
  }
1061
+ /**
1062
+ * No CASCADE: when something outside the sync inventory (a user view, an
1063
+ * unmanaged FK) still depends on the table PostgreSQL refuses, and schema
1064
+ * sync reports that as an error entry instead of silently dropping the
1065
+ * dependents.
1066
+ */
869
1067
  async dropTable() {
870
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())} CASCADE`;
1068
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())}`;
871
1069
  this._log(ddl);
872
1070
  await this._exec().exec(ddl);
873
1071
  }
@@ -900,7 +1098,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
900
1098
  }
901
1099
  }
902
1100
  async dropTableByName(tableName) {
903
- const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
1101
+ const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)}`;
904
1102
  this._log(ddl);
905
1103
  await this._exec().exec(ddl);
906
1104
  }
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.129",
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": "^0.1.129",
59
+ "@atscript/db-sql-tools": "^0.1.129"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "pg": {