@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 +126 -18
- package/dist/index.d.cts +38 -3
- package/dist/index.d.mts +38 -3
- package/dist/index.mjs +128 -20
- package/package.json +10 -10
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
|
-
/**
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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())}
|
|
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)}
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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())}
|
|
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)}
|
|
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.
|
|
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.
|
|
47
|
-
"@atscript/typescript": "^0.1.
|
|
46
|
+
"@atscript/core": "^0.1.92",
|
|
47
|
+
"@atscript/typescript": "^0.1.92",
|
|
48
48
|
"@types/pg": "^8.11.0",
|
|
49
|
-
"@uniqu/core": "^0.1.
|
|
49
|
+
"@uniqu/core": "^0.1.8",
|
|
50
50
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.1.
|
|
51
|
+
"unplugin-atscript": "^0.1.92"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1.
|
|
55
|
-
"@atscript/typescript": "^0.1.
|
|
56
|
-
"@uniqu/core": "^0.1.
|
|
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.
|
|
59
|
-
"@atscript/db
|
|
58
|
+
"@atscript/db-sql-tools": "^0.1.128",
|
|
59
|
+
"@atscript/db": "^0.1.128"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|