@atscript/db-postgres 0.1.38
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/LICENSE +21 -0
- package/dist/index.cjs +1354 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.mjs +1326 -0
- package/dist/plugin.cjs +73 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.mjs +47 -0
- package/package.json +83 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
const __atscript_db = __toESM(require("@atscript/db"));
|
|
26
|
+
const __atscript_db_sql_tools = __toESM(require("@atscript/db-sql-tools"));
|
|
27
|
+
const __atscript_core = __toESM(require("@atscript/core"));
|
|
28
|
+
|
|
29
|
+
//#region packages/db-postgres/src/sql-builder.ts
|
|
30
|
+
function defaultValueForType(designType) {
|
|
31
|
+
if (designType === "boolean") return "false";
|
|
32
|
+
if (designType === "json" || designType === "object") return "'{}'";
|
|
33
|
+
if (designType === "array") return "'[]'";
|
|
34
|
+
return (0, __atscript_db_sql_tools.defaultValueForType)(designType);
|
|
35
|
+
}
|
|
36
|
+
function defaultValueToSqlLiteral(designType, value) {
|
|
37
|
+
if (designType === "boolean") return value === "true" || value === "1" ? "true" : "false";
|
|
38
|
+
return (0, __atscript_db_sql_tools.defaultValueToSqlLiteral)(designType, value);
|
|
39
|
+
}
|
|
40
|
+
function esc(name) {
|
|
41
|
+
return name.replace(/"/g, "\"\"");
|
|
42
|
+
}
|
|
43
|
+
function qi(name) {
|
|
44
|
+
return `"${esc(name)}"`;
|
|
45
|
+
}
|
|
46
|
+
function quoteTableName(name) {
|
|
47
|
+
const dot = name.indexOf(".");
|
|
48
|
+
if (dot >= 0) return `${qi(name.slice(0, dot))}.${qi(name.slice(dot + 1))}`;
|
|
49
|
+
return qi(name);
|
|
50
|
+
}
|
|
51
|
+
/** Converts JS values to SQL-bindable params, keeping booleans native. */ function toPgValue(value) {
|
|
52
|
+
if (value === undefined) return null;
|
|
53
|
+
if (value === null) return null;
|
|
54
|
+
if (value instanceof Date) return value.toISOString();
|
|
55
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
const pgDialect = {
|
|
59
|
+
quoteIdentifier(name) {
|
|
60
|
+
return qi(name);
|
|
61
|
+
},
|
|
62
|
+
quoteTable(name) {
|
|
63
|
+
return quoteTableName(name);
|
|
64
|
+
},
|
|
65
|
+
unlimitedLimit: "ALL",
|
|
66
|
+
toValue: toPgValue,
|
|
67
|
+
toParam(value) {
|
|
68
|
+
if (value === undefined) return null;
|
|
69
|
+
return value;
|
|
70
|
+
},
|
|
71
|
+
regex(quotedCol, value) {
|
|
72
|
+
const pattern = value instanceof RegExp ? value.source : String(value);
|
|
73
|
+
return {
|
|
74
|
+
sql: `${quotedCol} ~ ?`,
|
|
75
|
+
params: [pattern]
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
79
|
+
paramPlaceholder(index) {
|
|
80
|
+
return `$${index}`;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function buildInsert(table, data) {
|
|
84
|
+
return (0, __atscript_db_sql_tools.buildInsert)(pgDialect, table, data);
|
|
85
|
+
}
|
|
86
|
+
function buildSelect(table, where, controls) {
|
|
87
|
+
return (0, __atscript_db_sql_tools.buildSelect)(pgDialect, table, where, controls);
|
|
88
|
+
}
|
|
89
|
+
function buildUpdate(table, data, where, limit) {
|
|
90
|
+
return (0, __atscript_db_sql_tools.buildUpdate)(pgDialect, table, data, where, limit);
|
|
91
|
+
}
|
|
92
|
+
function buildDelete(table, where, limit) {
|
|
93
|
+
return (0, __atscript_db_sql_tools.buildDelete)(pgDialect, table, where, limit);
|
|
94
|
+
}
|
|
95
|
+
function buildCreateView(viewName, plan, columns, resolveFieldRef) {
|
|
96
|
+
return (0, __atscript_db_sql_tools.buildCreateView)(pgDialect, viewName, plan, columns, resolveFieldRef);
|
|
97
|
+
}
|
|
98
|
+
function buildAggregateSelect(table, where, controls) {
|
|
99
|
+
return (0, __atscript_db_sql_tools.buildAggregateSelect)(pgDialect, table, where, controls);
|
|
100
|
+
}
|
|
101
|
+
function buildAggregateCount(table, where, controls) {
|
|
102
|
+
return (0, __atscript_db_sql_tools.buildAggregateCount)(pgDialect, table, where, controls);
|
|
103
|
+
}
|
|
104
|
+
function collationToPg(collation) {
|
|
105
|
+
switch (collation) {
|
|
106
|
+
case "binary": return "\"C\"";
|
|
107
|
+
case "nocase": return null;
|
|
108
|
+
case "unicode": return "\"und-x-icu\"";
|
|
109
|
+
default: return "\"und-x-icu\"";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Maps integer primitive tags to PostgreSQL integer types.
|
|
113
|
+
* Unsigned types are promoted to the next-larger PG type because
|
|
114
|
+
* PostgreSQL only has signed integer types:
|
|
115
|
+
* uint16 (0-65535) → INTEGER (not SMALLINT which caps at 32767)
|
|
116
|
+
* uint32 (0-4.3B) → BIGINT (not INTEGER which caps at ~2.1B)
|
|
117
|
+
*/ function intTypeFromTags(tags) {
|
|
118
|
+
if (tags?.has("int8") || tags?.has("byte")) return "SMALLINT";
|
|
119
|
+
if (tags?.has("uint8")) return "SMALLINT";
|
|
120
|
+
if (tags?.has("int16") || tags?.has("port")) return "SMALLINT";
|
|
121
|
+
if (tags?.has("uint16")) return "INTEGER";
|
|
122
|
+
if (tags?.has("int32")) return "INTEGER";
|
|
123
|
+
if (tags?.has("uint32")) return "BIGINT";
|
|
124
|
+
if (tags?.has("int64") || tags?.has("uint64")) return "BIGINT";
|
|
125
|
+
return "INTEGER";
|
|
126
|
+
}
|
|
127
|
+
function pgTypeFromField(field) {
|
|
128
|
+
if (field.fkTargetField) return pgTypeFromField(field.fkTargetField);
|
|
129
|
+
const tags = field.type?.type?.tags;
|
|
130
|
+
const metadata = field.type?.metadata;
|
|
131
|
+
const pgTypeOverride = metadata?.get("db.pg.type");
|
|
132
|
+
if (pgTypeOverride) return pgTypeOverride;
|
|
133
|
+
const precision = metadata?.get("db.column.precision");
|
|
134
|
+
switch (field.designType) {
|
|
135
|
+
case "number": {
|
|
136
|
+
if (precision) return `NUMERIC(${precision.precision},${precision.scale})`;
|
|
137
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") return "BIGINT";
|
|
138
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "now") return "BIGINT";
|
|
139
|
+
if (tags?.has("int")) return intTypeFromTags(tags);
|
|
140
|
+
return "DOUBLE PRECISION";
|
|
141
|
+
}
|
|
142
|
+
case "integer": return intTypeFromTags(tags);
|
|
143
|
+
case "decimal": {
|
|
144
|
+
if (precision) return `NUMERIC(${precision.precision},${precision.scale})`;
|
|
145
|
+
return "NUMERIC(10,2)";
|
|
146
|
+
}
|
|
147
|
+
case "boolean": return "BOOLEAN";
|
|
148
|
+
case "string": {
|
|
149
|
+
if (field.collate === "nocase" && !metadata?.get("db.pg.collate")) return "CITEXT";
|
|
150
|
+
if (tags?.has("char")) return "CHAR(1)";
|
|
151
|
+
const maxLen = metadata?.get("expect.maxLength")?.length;
|
|
152
|
+
if (maxLen !== undefined) return `VARCHAR(${maxLen})`;
|
|
153
|
+
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
154
|
+
return "TEXT";
|
|
155
|
+
}
|
|
156
|
+
case "json":
|
|
157
|
+
case "object":
|
|
158
|
+
case "array": return "JSONB";
|
|
159
|
+
default: {
|
|
160
|
+
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
161
|
+
return "TEXT";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function buildCreateTable(table, fields, foreignKeys, options) {
|
|
166
|
+
const colDefs = [];
|
|
167
|
+
const primaryKeys = fields.filter((f) => f.isPrimaryKey);
|
|
168
|
+
for (const field of fields) {
|
|
169
|
+
if (field.ignored) continue;
|
|
170
|
+
const sqlType = options?.typeMapper?.(field) ?? pgTypeFromField(field);
|
|
171
|
+
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
172
|
+
if (options?.incrementFields?.has(field.physicalName)) {
|
|
173
|
+
const start = options.autoIncrementStart;
|
|
174
|
+
def += start !== undefined ? ` GENERATED BY DEFAULT AS IDENTITY (START WITH ${start})` : " GENERATED BY DEFAULT AS IDENTITY";
|
|
175
|
+
}
|
|
176
|
+
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.physicalName)) def += " NOT NULL";
|
|
177
|
+
if (field.defaultValue?.kind === "value") def += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
178
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
179
|
+
if (field.defaultValue.fn === "uuid") def += " DEFAULT gen_random_uuid()";
|
|
180
|
+
else if (field.defaultValue.fn === "now") def += " DEFAULT (extract(epoch from now()) * 1000)::bigint";
|
|
181
|
+
}
|
|
182
|
+
const nativeCollate = field.type?.metadata?.get("db.pg.collate");
|
|
183
|
+
if (nativeCollate) def += ` COLLATE "${nativeCollate}"`;
|
|
184
|
+
else if (field.collate) {
|
|
185
|
+
const pgCollate = collationToPg(field.collate);
|
|
186
|
+
if (pgCollate) def += ` COLLATE ${pgCollate}`;
|
|
187
|
+
}
|
|
188
|
+
colDefs.push(def);
|
|
189
|
+
}
|
|
190
|
+
if (primaryKeys.length === 1) {
|
|
191
|
+
const pkCol = qi(primaryKeys[0].physicalName);
|
|
192
|
+
for (let i = 0; i < colDefs.length; i++) if (colDefs[i].startsWith(`${pkCol} `)) {
|
|
193
|
+
colDefs[i] += " PRIMARY KEY";
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
} else if (primaryKeys.length > 1) {
|
|
197
|
+
const pkCols = primaryKeys.map((pk) => qi(pk.physicalName)).join(", ");
|
|
198
|
+
colDefs.push(`PRIMARY KEY (${pkCols})`);
|
|
199
|
+
}
|
|
200
|
+
if (foreignKeys) for (const fk of foreignKeys.values()) {
|
|
201
|
+
const localCols = fk.fields.map((f) => qi(f)).join(", ");
|
|
202
|
+
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
203
|
+
let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
|
|
204
|
+
if (fk.onDelete) constraint += ` ON DELETE ${(0, __atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
|
|
205
|
+
if (fk.onUpdate) constraint += ` ON UPDATE ${(0, __atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
|
|
206
|
+
colDefs.push(constraint);
|
|
207
|
+
}
|
|
208
|
+
return `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${colDefs.join(", ")})`;
|
|
209
|
+
}
|
|
210
|
+
function offsetPlaceholders(fragment, offset) {
|
|
211
|
+
if (offset === 0) return fragment;
|
|
212
|
+
const sql = fragment.sql.replace(/\$(\d+)/g, (_, n) => `$${Number(n) + offset}`);
|
|
213
|
+
return {
|
|
214
|
+
sql,
|
|
215
|
+
params: fragment.params
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region packages/db-postgres/src/filter-builder.ts
|
|
221
|
+
function buildWhere(filter) {
|
|
222
|
+
return (0, __atscript_db_sql_tools.buildWhere)(pgDialect, filter);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region packages/db-postgres/src/postgres-adapter.ts
|
|
227
|
+
function _define_property$1(obj, key, value) {
|
|
228
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
229
|
+
value,
|
|
230
|
+
enumerable: true,
|
|
231
|
+
configurable: true,
|
|
232
|
+
writable: true
|
|
233
|
+
});
|
|
234
|
+
else obj[key] = value;
|
|
235
|
+
return obj;
|
|
236
|
+
}
|
|
237
|
+
/** PostgreSQL COUNT() may return string (bigint) — parse to number. */ function parseCount(value) {
|
|
238
|
+
if (typeof value === "string") return Number.parseInt(value, 10);
|
|
239
|
+
return value ?? 0;
|
|
240
|
+
}
|
|
241
|
+
var PostgresAdapter = class PostgresAdapter extends __atscript_db.BaseDbAdapter {
|
|
242
|
+
/** Schema name for queries (null falls through to 'public'). */ get _schema() {
|
|
243
|
+
return this._table.schema ?? null;
|
|
244
|
+
}
|
|
245
|
+
async _beginTransaction() {
|
|
246
|
+
const conn = await this.driver.getConnection();
|
|
247
|
+
try {
|
|
248
|
+
await conn.exec("BEGIN");
|
|
249
|
+
this._log("BEGIN");
|
|
250
|
+
return conn;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
conn.release();
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async _commitTransaction(state) {
|
|
257
|
+
const conn = state;
|
|
258
|
+
try {
|
|
259
|
+
this._log("COMMIT");
|
|
260
|
+
await conn.exec("COMMIT");
|
|
261
|
+
} finally {
|
|
262
|
+
conn.release();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async _rollbackTransaction(state) {
|
|
266
|
+
const conn = state;
|
|
267
|
+
try {
|
|
268
|
+
this._log("ROLLBACK");
|
|
269
|
+
await conn.exec("ROLLBACK");
|
|
270
|
+
} finally {
|
|
271
|
+
conn.release();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Returns the active executor: dedicated connection if inside a transaction,
|
|
276
|
+
* otherwise the pool-based driver.
|
|
277
|
+
*/ _exec() {
|
|
278
|
+
const txState = this._getTransactionState();
|
|
279
|
+
return txState ?? this.driver;
|
|
280
|
+
}
|
|
281
|
+
/** PostgreSQL enforces FK constraints natively. */ supportsNativeForeignKeys() {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
prepareId(id, _fieldType) {
|
|
285
|
+
return id;
|
|
286
|
+
}
|
|
287
|
+
supportsNativeValueDefaults() {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
nativeDefaultFns() {
|
|
291
|
+
return PostgresAdapter.NATIVE_DEFAULT_FNS;
|
|
292
|
+
}
|
|
293
|
+
onBeforeFlatten(_type) {}
|
|
294
|
+
onAfterFlatten() {
|
|
295
|
+
for (const fd of this._table.fieldDescriptors) if (fd.collate === "nocase") this._nocaseColumns.add(fd.physicalName);
|
|
296
|
+
}
|
|
297
|
+
onFieldScanned(field, _type, metadata) {
|
|
298
|
+
if (metadata.has("db.default.increment")) {
|
|
299
|
+
this._incrementFields.add(field);
|
|
300
|
+
const startVal = metadata.get("db.default.increment");
|
|
301
|
+
if (typeof startVal === "number") this._autoIncrementStart = startVal;
|
|
302
|
+
}
|
|
303
|
+
const vectorMeta = metadata.get("db.search.vector");
|
|
304
|
+
if (vectorMeta) {
|
|
305
|
+
const indexName = vectorMeta.indexName || field;
|
|
306
|
+
this._vectorFields.set(field, {
|
|
307
|
+
dimensions: vectorMeta.dimensions,
|
|
308
|
+
similarity: vectorMeta.similarity || "cosine",
|
|
309
|
+
indexName
|
|
310
|
+
});
|
|
311
|
+
const threshold = metadata.get("db.search.vector.threshold");
|
|
312
|
+
if (threshold !== undefined) this._vectorThresholds.set(indexName, threshold);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
getDesiredTableOptions() {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
async getExistingTableOptions() {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Converts vector fields between JavaScript `number[]` and pgvector text format `[1,2,3]`.
|
|
323
|
+
* The pg driver serializes JS arrays as PostgreSQL array literals `{1,2,3}` which is
|
|
324
|
+
* invalid for the pgvector `vector` type — it expects bracket-delimited `[1,2,3]`.
|
|
325
|
+
*/ formatValue(field) {
|
|
326
|
+
if (!this._vectorFields.has(field.path)) return undefined;
|
|
327
|
+
return {
|
|
328
|
+
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
329
|
+
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
334
|
+
* and rethrow as structured `DbError`.
|
|
335
|
+
*
|
|
336
|
+
* PostgreSQL uses SQLSTATE codes:
|
|
337
|
+
* - 23505 = unique_violation
|
|
338
|
+
* - 23503 = foreign_key_violation
|
|
339
|
+
*/ async _wrapConstraintError(fn) {
|
|
340
|
+
try {
|
|
341
|
+
return await fn();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
344
|
+
const err = error;
|
|
345
|
+
if (err.code === "23505") {
|
|
346
|
+
const field = this._extractFieldFromConstraint(err.constraint) ?? "";
|
|
347
|
+
throw new __atscript_db.DbError("CONFLICT", [{
|
|
348
|
+
path: field,
|
|
349
|
+
message: err.detail ?? err.message
|
|
350
|
+
}]);
|
|
351
|
+
}
|
|
352
|
+
if (err.code === "23503") {
|
|
353
|
+
const errors = this._mapFkError(err.detail ?? err.message, err.constraint);
|
|
354
|
+
throw new __atscript_db.DbError("FK_VIOLATION", errors);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
_extractFieldFromConstraint(constraint) {
|
|
361
|
+
if (!constraint) return undefined;
|
|
362
|
+
const tableName = this._table.tableName;
|
|
363
|
+
if (constraint.startsWith(`${tableName}_`) && constraint.endsWith("_key")) {
|
|
364
|
+
const fieldPart = constraint.slice(tableName.length + 1, -4);
|
|
365
|
+
const fd = this._table.fieldDescriptors.find((f) => f.physicalName === fieldPart);
|
|
366
|
+
if (fd) return fd.path;
|
|
367
|
+
}
|
|
368
|
+
return constraint;
|
|
369
|
+
}
|
|
370
|
+
_mapFkError(detail, constraint) {
|
|
371
|
+
const fkMatch = detail.match(/Key \(([^)]+)\)/);
|
|
372
|
+
if (fkMatch) {
|
|
373
|
+
const physicalCol = fkMatch[1].split(",")[0].trim();
|
|
374
|
+
const field = this._table.fieldDescriptors.find((f) => f.physicalName === physicalCol);
|
|
375
|
+
return [{
|
|
376
|
+
path: field?.path ?? physicalCol,
|
|
377
|
+
message: detail
|
|
378
|
+
}];
|
|
379
|
+
}
|
|
380
|
+
return [{
|
|
381
|
+
path: constraint ?? "",
|
|
382
|
+
message: detail
|
|
383
|
+
}];
|
|
384
|
+
}
|
|
385
|
+
async insertOne(data) {
|
|
386
|
+
let { sql, params } = buildInsert(this.resolveTableName(), data);
|
|
387
|
+
const pkCols = this._table.primaryKeys.map((pk) => qi(pk));
|
|
388
|
+
if (pkCols.length > 0) sql += ` RETURNING ${pkCols.join(", ")}`;
|
|
389
|
+
this._log(sql, params);
|
|
390
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
391
|
+
const returned = result.rows?.[0];
|
|
392
|
+
return { insertedId: this._resolveInsertedId(data, returned ? Object.values(returned)[0] : undefined) };
|
|
393
|
+
}
|
|
394
|
+
async insertMany(data) {
|
|
395
|
+
if (data.length === 0) return {
|
|
396
|
+
insertedCount: 0,
|
|
397
|
+
insertedIds: []
|
|
398
|
+
};
|
|
399
|
+
return this.withTransaction(async () => {
|
|
400
|
+
const tableName = this.resolveTableName();
|
|
401
|
+
const pkCols = this._table.primaryKeys;
|
|
402
|
+
const returningSuffix = pkCols.length > 0 ? ` RETURNING ${pkCols.map((pk) => qi(pk)).join(", ")}` : "";
|
|
403
|
+
const keys = Object.keys(data[0]);
|
|
404
|
+
const colsClause = keys.map((k) => qi(k)).join(", ");
|
|
405
|
+
const paramsPerRow = keys.length;
|
|
406
|
+
const maxRowsPerBatch = paramsPerRow > 0 ? Math.floor(6e4 / paramsPerRow) : data.length;
|
|
407
|
+
const allIds = [];
|
|
408
|
+
for (let offset = 0; offset < data.length; offset += maxRowsPerBatch) {
|
|
409
|
+
const batchEnd = Math.min(offset + maxRowsPerBatch, data.length);
|
|
410
|
+
const batchSize = batchEnd - offset;
|
|
411
|
+
const params = [];
|
|
412
|
+
const valuesClauses = [];
|
|
413
|
+
for (let i = offset; i < batchEnd; i++) {
|
|
414
|
+
const row = data[i];
|
|
415
|
+
const rowPlaceholders = [];
|
|
416
|
+
for (const k of keys) {
|
|
417
|
+
params.push(pgDialect.toValue(row[k]));
|
|
418
|
+
rowPlaceholders.push(`$${params.length}`);
|
|
419
|
+
}
|
|
420
|
+
valuesClauses.push(`(${rowPlaceholders.join(", ")})`);
|
|
421
|
+
}
|
|
422
|
+
const sql = `INSERT INTO ${quoteTableName(tableName)} (${colsClause}) VALUES ${valuesClauses.join(", ")}${returningSuffix}`;
|
|
423
|
+
this._log(sql, params);
|
|
424
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
425
|
+
for (let i = 0; i < batchSize; i++) {
|
|
426
|
+
const returned = result.rows?.[i];
|
|
427
|
+
allIds.push(this._resolveInsertedId(data[offset + i], returned ? Object.values(returned)[0] : undefined));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
insertedCount: allIds.length,
|
|
432
|
+
insertedIds: allIds
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
async findOne(query) {
|
|
437
|
+
const where = buildWhere(query.filter);
|
|
438
|
+
const controls = {
|
|
439
|
+
...query.controls,
|
|
440
|
+
$limit: 1
|
|
441
|
+
};
|
|
442
|
+
const { sql, params } = buildSelect(this.resolveTableName(), where, controls);
|
|
443
|
+
this._log(sql, params);
|
|
444
|
+
return this._exec().get(sql, params);
|
|
445
|
+
}
|
|
446
|
+
async findMany(query) {
|
|
447
|
+
const where = buildWhere(query.filter);
|
|
448
|
+
const { sql, params } = buildSelect(this.resolveTableName(), where, query.controls);
|
|
449
|
+
this._log(sql, params);
|
|
450
|
+
return this._exec().all(sql, params);
|
|
451
|
+
}
|
|
452
|
+
async count(query) {
|
|
453
|
+
const where = buildWhere(query.filter);
|
|
454
|
+
const tableName = this.resolveTableName();
|
|
455
|
+
const raw = {
|
|
456
|
+
sql: `SELECT COUNT(*) as cnt FROM ${quoteTableName(tableName)} WHERE ${where.sql}`,
|
|
457
|
+
params: where.params
|
|
458
|
+
};
|
|
459
|
+
const { sql, params } = (0, __atscript_db_sql_tools.finalizeParams)(pgDialect, raw);
|
|
460
|
+
this._log(sql, params);
|
|
461
|
+
const row = await this._exec().get(sql, params);
|
|
462
|
+
return parseCount(row?.cnt);
|
|
463
|
+
}
|
|
464
|
+
async aggregate(query) {
|
|
465
|
+
const where = buildWhere(query.filter);
|
|
466
|
+
const tableName = this.resolveTableName();
|
|
467
|
+
if (query.controls.$count) {
|
|
468
|
+
const { sql: sql$1, params: params$1 } = buildAggregateCount(tableName, where, query.controls);
|
|
469
|
+
this._log(sql$1, params$1);
|
|
470
|
+
const row = await this._exec().get(sql$1, params$1);
|
|
471
|
+
const count = parseCount(row?.count);
|
|
472
|
+
return [{ count }];
|
|
473
|
+
}
|
|
474
|
+
const { sql, params } = buildAggregateSelect(tableName, where, query.controls);
|
|
475
|
+
this._log(sql, params);
|
|
476
|
+
return this._exec().all(sql, params);
|
|
477
|
+
}
|
|
478
|
+
async updateOne(filter, data) {
|
|
479
|
+
const where = buildWhere(filter);
|
|
480
|
+
const tableName = this.resolveTableName();
|
|
481
|
+
const keys = Object.keys(data);
|
|
482
|
+
const setClauses = keys.map((k, i) => `${qi(k)} = $${i + 1}`);
|
|
483
|
+
const setParams = keys.map((k) => pgDialect.toValue(data[k]));
|
|
484
|
+
const finalizedWhere = (0, __atscript_db_sql_tools.finalizeParams)(pgDialect, where);
|
|
485
|
+
const offsetWhere = offsetPlaceholders(finalizedWhere, keys.length);
|
|
486
|
+
const sql = `UPDATE ${quoteTableName(tableName)} SET ${setClauses.join(", ")} WHERE ctid = (SELECT ctid FROM ${quoteTableName(tableName)} WHERE ${offsetWhere.sql} LIMIT 1)`;
|
|
487
|
+
const params = [...setParams, ...where.params];
|
|
488
|
+
this._log(sql, params);
|
|
489
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
490
|
+
return {
|
|
491
|
+
matchedCount: result.affectedRows,
|
|
492
|
+
modifiedCount: result.affectedRows
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async updateMany(filter, data) {
|
|
496
|
+
const where = buildWhere(filter);
|
|
497
|
+
const { sql, params } = buildUpdate(this.resolveTableName(), data, where);
|
|
498
|
+
this._log(sql, params);
|
|
499
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
500
|
+
return {
|
|
501
|
+
matchedCount: result.affectedRows,
|
|
502
|
+
modifiedCount: result.affectedRows
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async replaceOne(filter, data) {
|
|
506
|
+
return this.updateOne(filter, data);
|
|
507
|
+
}
|
|
508
|
+
async replaceMany(filter, data) {
|
|
509
|
+
return this.updateMany(filter, data);
|
|
510
|
+
}
|
|
511
|
+
async deleteOne(filter) {
|
|
512
|
+
const where = buildWhere(filter);
|
|
513
|
+
const tableName = this.resolveTableName();
|
|
514
|
+
const raw = {
|
|
515
|
+
sql: `DELETE FROM ${quoteTableName(tableName)} WHERE ctid = (SELECT ctid FROM ${quoteTableName(tableName)} WHERE ${where.sql} LIMIT 1)`,
|
|
516
|
+
params: where.params
|
|
517
|
+
};
|
|
518
|
+
const { sql, params } = (0, __atscript_db_sql_tools.finalizeParams)(pgDialect, raw);
|
|
519
|
+
this._log(sql, params);
|
|
520
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
521
|
+
return { deletedCount: result.affectedRows };
|
|
522
|
+
}
|
|
523
|
+
async deleteMany(filter) {
|
|
524
|
+
const where = buildWhere(filter);
|
|
525
|
+
const { sql, params } = buildDelete(this.resolveTableName(), where);
|
|
526
|
+
this._log(sql, params);
|
|
527
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
528
|
+
return { deletedCount: result.affectedRows };
|
|
529
|
+
}
|
|
530
|
+
async ensureTable() {
|
|
531
|
+
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
532
|
+
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
|
|
533
|
+
this._citextProvisioned = true;
|
|
534
|
+
} catch (err) {
|
|
535
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
536
|
+
throw new Error(`Failed to create citext extension for @db.collate 'nocase' columns: ${msg}. ` + `Either run 'CREATE EXTENSION citext' as a superuser, or use @db.pg.type "CITEXT" after provisioning the extension manually.`);
|
|
537
|
+
}
|
|
538
|
+
if (this._supportsVector === undefined && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
539
|
+
if (this._table instanceof __atscript_db.AtscriptDbView) return this._ensureView();
|
|
540
|
+
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
541
|
+
incrementFields: this._incrementFields,
|
|
542
|
+
autoIncrementStart: this._autoIncrementStart,
|
|
543
|
+
typeMapper: (field) => this.typeMapper(field)
|
|
544
|
+
});
|
|
545
|
+
this._log(sql);
|
|
546
|
+
await this._exec().exec(sql);
|
|
547
|
+
}
|
|
548
|
+
async _ensureView() {
|
|
549
|
+
const view = this._table;
|
|
550
|
+
const sql = buildCreateView(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref, qi));
|
|
551
|
+
this._log(sql);
|
|
552
|
+
await this._exec().exec(sql);
|
|
553
|
+
}
|
|
554
|
+
async getExistingColumns() {
|
|
555
|
+
return this.getExistingColumnsForTable(this._table.tableName);
|
|
556
|
+
}
|
|
557
|
+
async getExistingColumnsForTable(tableName) {
|
|
558
|
+
const schema = this._schema;
|
|
559
|
+
const rows = await this._exec().all(`SELECT c.column_name, c.data_type, c.udt_name, c.character_maximum_length, c.numeric_precision, c.numeric_scale, c.is_nullable, c.column_default, c.is_identity,
|
|
560
|
+
format_type(a.atttypid, a.atttypmod) AS formatted_type
|
|
561
|
+
FROM information_schema.columns c
|
|
562
|
+
JOIN pg_attribute a ON a.attname = c.column_name
|
|
563
|
+
AND a.attrelid = (SELECT oid FROM pg_class WHERE relname = $1 AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = COALESCE($2, 'public')))
|
|
564
|
+
WHERE c.table_name = $1 AND c.table_schema = COALESCE($2, 'public')
|
|
565
|
+
ORDER BY c.ordinal_position`, [tableName, schema]);
|
|
566
|
+
const pkRows = await this._exec().all(`SELECT kcu.column_name
|
|
567
|
+
FROM information_schema.table_constraints tc
|
|
568
|
+
JOIN information_schema.key_column_usage kcu
|
|
569
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
570
|
+
WHERE tc.table_name = $1 AND tc.table_schema = COALESCE($2, 'public')
|
|
571
|
+
AND tc.constraint_type = 'PRIMARY KEY'`, [tableName, schema]);
|
|
572
|
+
const pkSet = new Set(pkRows.map((r) => r.column_name));
|
|
573
|
+
return rows.map((r) => ({
|
|
574
|
+
name: r.column_name,
|
|
575
|
+
type: normalizePgType(r.data_type, r.character_maximum_length, r.numeric_precision, r.numeric_scale, r.udt_name, r.formatted_type),
|
|
576
|
+
notnull: r.is_nullable === "NO",
|
|
577
|
+
pk: pkSet.has(r.column_name),
|
|
578
|
+
dflt_value: normalizePgDefault(r.column_default, r.is_identity)
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
async syncColumns(diff) {
|
|
582
|
+
if (this._nocaseColumns.size > 0) await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
|
|
583
|
+
const tableName = this.resolveTableName();
|
|
584
|
+
const added = [];
|
|
585
|
+
const renamed = [];
|
|
586
|
+
for (const { field, oldName } of diff.renamed ?? []) {
|
|
587
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
|
|
588
|
+
this._log(ddl);
|
|
589
|
+
await this._exec().exec(ddl);
|
|
590
|
+
renamed.push(field.physicalName);
|
|
591
|
+
}
|
|
592
|
+
for (const field of diff.added) {
|
|
593
|
+
const sqlType = this.typeMapper(field);
|
|
594
|
+
let ddl = `ALTER TABLE ${quoteTableName(tableName)} ADD COLUMN ${qi(field.physicalName)} ${sqlType}`;
|
|
595
|
+
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") ddl += " GENERATED BY DEFAULT AS IDENTITY";
|
|
596
|
+
else {
|
|
597
|
+
if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
|
|
598
|
+
if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
599
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
600
|
+
if (field.defaultValue.fn === "uuid") ddl += " DEFAULT gen_random_uuid()";
|
|
601
|
+
else if (field.defaultValue.fn === "now") ddl += " DEFAULT (extract(epoch from now()) * 1000)::bigint";
|
|
602
|
+
} else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType(field.designType)}`;
|
|
603
|
+
}
|
|
604
|
+
if (field.collate) {
|
|
605
|
+
const nativeCollate = field.type?.metadata?.get("db.pg.collate");
|
|
606
|
+
if (nativeCollate) ddl += ` COLLATE "${nativeCollate}"`;
|
|
607
|
+
else {
|
|
608
|
+
const pgCollate = collationToPg(field.collate);
|
|
609
|
+
if (pgCollate) ddl += ` COLLATE ${pgCollate}`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
this._log(ddl);
|
|
613
|
+
await this._exec().exec(ddl);
|
|
614
|
+
added.push(field.physicalName);
|
|
615
|
+
}
|
|
616
|
+
for (const { field } of diff.typeChanged ?? []) {
|
|
617
|
+
const sqlType = this.typeMapper(field);
|
|
618
|
+
const col = qi(field.physicalName);
|
|
619
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${col}::text::${sqlType}`;
|
|
620
|
+
this._log(ddl);
|
|
621
|
+
await this._exec().exec(ddl);
|
|
622
|
+
}
|
|
623
|
+
for (const { field } of diff.nullableChanged ?? []) {
|
|
624
|
+
if (!field.optional) {
|
|
625
|
+
const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
|
|
626
|
+
const backfill = `UPDATE ${quoteTableName(tableName)} SET ${qi(field.physicalName)} = ${fallback} WHERE ${qi(field.physicalName)} IS NULL`;
|
|
627
|
+
this._log(backfill);
|
|
628
|
+
await this._exec().exec(backfill);
|
|
629
|
+
}
|
|
630
|
+
const nullability = field.optional ? "DROP NOT NULL" : "SET NOT NULL";
|
|
631
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} ${nullability}`;
|
|
632
|
+
this._log(ddl);
|
|
633
|
+
await this._exec().exec(ddl);
|
|
634
|
+
}
|
|
635
|
+
for (const { field } of diff.defaultChanged ?? []) {
|
|
636
|
+
let ddl;
|
|
637
|
+
if (field.defaultValue?.kind === "value") ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
|
|
638
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
639
|
+
const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : field.defaultValue.fn === "uuid" ? "gen_random_uuid()" : `${field.defaultValue.fn}()`;
|
|
640
|
+
ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${fnExpr}`;
|
|
641
|
+
} else ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
|
|
642
|
+
this._log(ddl);
|
|
643
|
+
await this._exec().exec(ddl);
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
added,
|
|
647
|
+
renamed
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
async recreateTable() {
|
|
651
|
+
const tableName = this.resolveTableName();
|
|
652
|
+
const schema = this._schema;
|
|
653
|
+
const baseTempName = `${this._table.tableName}__tmp_${Date.now()}`;
|
|
654
|
+
const tempName = schema ? `${schema}.${baseTempName}` : baseTempName;
|
|
655
|
+
const conn = await this.driver.getConnection();
|
|
656
|
+
try {
|
|
657
|
+
await conn.exec("BEGIN");
|
|
658
|
+
const fkRefs = await conn.all(`SELECT tc.constraint_name, tc.table_name, tc.table_schema,
|
|
659
|
+
kcu.column_name, kcur.column_name AS ref_column_name,
|
|
660
|
+
rc.delete_rule, rc.update_rule
|
|
661
|
+
FROM information_schema.referential_constraints rc
|
|
662
|
+
JOIN information_schema.table_constraints tc
|
|
663
|
+
ON rc.constraint_name = tc.constraint_name AND rc.constraint_schema = tc.constraint_schema
|
|
664
|
+
JOIN information_schema.key_column_usage kcu
|
|
665
|
+
ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
|
|
666
|
+
JOIN information_schema.key_column_usage kcur
|
|
667
|
+
ON kcur.constraint_name = rc.unique_constraint_name AND kcur.table_schema = rc.unique_constraint_schema
|
|
668
|
+
AND kcur.ordinal_position = kcu.ordinal_position
|
|
669
|
+
WHERE rc.unique_constraint_schema = COALESCE($1, 'public')
|
|
670
|
+
AND rc.unique_constraint_name IN (
|
|
671
|
+
SELECT constraint_name FROM information_schema.table_constraints
|
|
672
|
+
WHERE table_name = $2 AND table_schema = COALESCE($1, 'public') AND constraint_type = 'PRIMARY KEY'
|
|
673
|
+
)`, [schema, this._table.tableName]);
|
|
674
|
+
const fkByName = new Map();
|
|
675
|
+
for (const fk of fkRefs) {
|
|
676
|
+
let entry = fkByName.get(fk.constraint_name);
|
|
677
|
+
if (!entry) {
|
|
678
|
+
entry = {
|
|
679
|
+
schema: fk.table_schema,
|
|
680
|
+
table: fk.table_name,
|
|
681
|
+
cols: [],
|
|
682
|
+
refCols: [],
|
|
683
|
+
onDelete: fk.delete_rule,
|
|
684
|
+
onUpdate: fk.update_rule
|
|
685
|
+
};
|
|
686
|
+
fkByName.set(fk.constraint_name, entry);
|
|
687
|
+
}
|
|
688
|
+
entry.cols.push(fk.column_name);
|
|
689
|
+
entry.refCols.push(fk.ref_column_name);
|
|
690
|
+
}
|
|
691
|
+
for (const [name, fk] of fkByName) {
|
|
692
|
+
const ddl = `ALTER TABLE ${qi(fk.schema)}.${qi(fk.table)} DROP CONSTRAINT IF EXISTS ${qi(name)}`;
|
|
693
|
+
this._log(ddl);
|
|
694
|
+
await conn.exec(ddl);
|
|
695
|
+
}
|
|
696
|
+
const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
697
|
+
incrementFields: this._incrementFields,
|
|
698
|
+
autoIncrementStart: this._autoIncrementStart,
|
|
699
|
+
typeMapper: (field) => this.typeMapper(field)
|
|
700
|
+
});
|
|
701
|
+
this._log(createSql);
|
|
702
|
+
await conn.exec(createSql);
|
|
703
|
+
const oldColRows = await conn.all(`SELECT column_name FROM information_schema.columns
|
|
704
|
+
WHERE table_name = $1 AND table_schema = COALESCE($2, 'public')
|
|
705
|
+
ORDER BY ordinal_position`, [this._table.tableName, schema]);
|
|
706
|
+
const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
|
|
707
|
+
const oldColSet = new Set(oldColRows.map((c) => c.column_name));
|
|
708
|
+
const commonCols = newCols.filter((c) => oldColSet.has(c));
|
|
709
|
+
if (commonCols.length > 0) {
|
|
710
|
+
const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
711
|
+
const colNames = commonCols.map((c) => qi(c)).join(", ");
|
|
712
|
+
const selectExprs = commonCols.map((c) => {
|
|
713
|
+
const field = fieldsByName.get(c);
|
|
714
|
+
if (field && !field.optional && !field.isPrimaryKey) {
|
|
715
|
+
const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
|
|
716
|
+
return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
|
|
717
|
+
}
|
|
718
|
+
return qi(c);
|
|
719
|
+
}).join(", ");
|
|
720
|
+
const copySql = `INSERT INTO ${quoteTableName(tempName)} (${colNames}) SELECT ${selectExprs} FROM ${quoteTableName(tableName)}`;
|
|
721
|
+
this._log(copySql);
|
|
722
|
+
await conn.exec(copySql);
|
|
723
|
+
}
|
|
724
|
+
await conn.exec(`DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`);
|
|
725
|
+
await conn.exec(`ALTER TABLE ${quoteTableName(tempName)} RENAME TO ${qi(this._table.tableName)}`);
|
|
726
|
+
const resolvedTable = this.resolveTableName();
|
|
727
|
+
for (const [, fk] of fkByName) {
|
|
728
|
+
const localCols = fk.cols.map((c) => qi(c)).join(", ");
|
|
729
|
+
const refCols = fk.refCols.map((c) => qi(c)).join(", ");
|
|
730
|
+
let ddl = `ALTER TABLE ${qi(fk.schema)}.${qi(fk.table)} ADD FOREIGN KEY (${localCols}) REFERENCES ${quoteTableName(resolvedTable)} (${refCols})`;
|
|
731
|
+
if (fk.onDelete !== "NO ACTION") ddl += ` ON DELETE ${fk.onDelete}`;
|
|
732
|
+
if (fk.onUpdate !== "NO ACTION") ddl += ` ON UPDATE ${fk.onUpdate}`;
|
|
733
|
+
this._log(ddl);
|
|
734
|
+
await conn.exec(ddl);
|
|
735
|
+
}
|
|
736
|
+
await conn.exec("COMMIT");
|
|
737
|
+
await this._resetIdentitySequences();
|
|
738
|
+
} catch (err) {
|
|
739
|
+
await conn.exec("ROLLBACK").catch(() => {});
|
|
740
|
+
throw err;
|
|
741
|
+
} finally {
|
|
742
|
+
conn.release();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async afterSyncTable() {
|
|
746
|
+
await this._resetIdentitySequences();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Resets IDENTITY sequences to MAX(column) so that the next auto-generated
|
|
750
|
+
* value doesn't conflict with existing data. PostgreSQL's GENERATED BY DEFAULT
|
|
751
|
+
* AS IDENTITY does not advance the sequence when rows are inserted with explicit
|
|
752
|
+
* values, so this is needed after data seeding, bulk imports, or recreateTable().
|
|
753
|
+
*/ async _resetIdentitySequences() {
|
|
754
|
+
if (this._incrementFields.size === 0) return;
|
|
755
|
+
const tableName = this.resolveTableName();
|
|
756
|
+
const emptyFallback = this._autoIncrementStart ?? 1;
|
|
757
|
+
for (const field of this._incrementFields) {
|
|
758
|
+
const col = this._table.fieldDescriptors.find((f) => f.path === field)?.physicalName ?? field;
|
|
759
|
+
const sql = `SELECT setval(pg_get_serial_sequence('${tableName}', '${col}'), COALESCE(MAX(${qi(col)}), ${emptyFallback}), MAX(${qi(col)}) IS NOT NULL) FROM ${quoteTableName(tableName)}`;
|
|
760
|
+
this._log(sql);
|
|
761
|
+
await this._exec().run(sql);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async tableExists() {
|
|
765
|
+
const row = 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]);
|
|
766
|
+
return row?.exists ?? false;
|
|
767
|
+
}
|
|
768
|
+
async dropTable() {
|
|
769
|
+
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())} CASCADE`;
|
|
770
|
+
this._log(ddl);
|
|
771
|
+
await this._exec().exec(ddl);
|
|
772
|
+
}
|
|
773
|
+
async dropColumns(columns) {
|
|
774
|
+
const tableName = this.resolveTableName();
|
|
775
|
+
const drops = columns.map((col) => `DROP COLUMN ${qi(col)}`).join(", ");
|
|
776
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ${drops}`;
|
|
777
|
+
this._log(ddl);
|
|
778
|
+
await this._exec().exec(ddl);
|
|
779
|
+
}
|
|
780
|
+
async dropTableByName(tableName) {
|
|
781
|
+
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
|
|
782
|
+
this._log(ddl);
|
|
783
|
+
await this._exec().exec(ddl);
|
|
784
|
+
}
|
|
785
|
+
async dropViewByName(viewName) {
|
|
786
|
+
const ddl = `DROP VIEW IF EXISTS ${quoteTableName(viewName)}`;
|
|
787
|
+
this._log(ddl);
|
|
788
|
+
await this._exec().exec(ddl);
|
|
789
|
+
}
|
|
790
|
+
async renameTable(oldName) {
|
|
791
|
+
const newName = this._table.tableName;
|
|
792
|
+
const ddl = `ALTER TABLE ${quoteTableName(oldName)} RENAME TO ${qi(newName)}`;
|
|
793
|
+
this._log(ddl);
|
|
794
|
+
await this._exec().exec(ddl);
|
|
795
|
+
}
|
|
796
|
+
typeMapper(field) {
|
|
797
|
+
if (this._vectorFields.has(field.path)) {
|
|
798
|
+
const vec = this._vectorFields.get(field.path);
|
|
799
|
+
return this._supportsVector ? `vector(${vec.dimensions})` : "JSONB";
|
|
800
|
+
}
|
|
801
|
+
return pgTypeFromField(field);
|
|
802
|
+
}
|
|
803
|
+
async syncIndexes() {
|
|
804
|
+
const tableName = this._table.tableName;
|
|
805
|
+
const schema = this._schema;
|
|
806
|
+
await this.syncIndexesWithDiff({
|
|
807
|
+
listExisting: async () => this._exec().all(`SELECT indexname AS name FROM pg_indexes
|
|
808
|
+
WHERE tablename = $1 AND schemaname = COALESCE($2, 'public')`, [tableName, schema]),
|
|
809
|
+
createIndex: async (index) => {
|
|
810
|
+
if (index.type === "fulltext") {
|
|
811
|
+
const tsvectorExpr = this._buildTsvectorExpr(index.fields);
|
|
812
|
+
const sql$1 = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gin(to_tsvector('english', ${tsvectorExpr}))`;
|
|
813
|
+
this._log(sql$1);
|
|
814
|
+
await this._exec().exec(sql$1);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
818
|
+
const cols = index.fields.map((f) => `${qi(f.name)} ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
819
|
+
const sql = `CREATE ${unique}INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${cols})`;
|
|
820
|
+
this._log(sql);
|
|
821
|
+
await this._exec().exec(sql);
|
|
822
|
+
},
|
|
823
|
+
dropIndex: async (name) => {
|
|
824
|
+
const schemaPrefix = schema ? `${qi(schema)}.` : "";
|
|
825
|
+
const sql = `DROP INDEX IF EXISTS ${schemaPrefix}${qi(name)}`;
|
|
826
|
+
this._log(sql);
|
|
827
|
+
await this._exec().exec(sql);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
831
|
+
const indexName = `atscript__vec_${vec.indexName}`;
|
|
832
|
+
const opsClass = similarityToPgOps(vec.similarity);
|
|
833
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(indexName)} ON ${quoteTableName(this.resolveTableName())} USING hnsw (${qi(field)} ${opsClass})`;
|
|
834
|
+
this._log(sql);
|
|
835
|
+
await this._exec().exec(sql);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async syncForeignKeys() {
|
|
839
|
+
const existingByName = await this._getExistingFkConstraints();
|
|
840
|
+
const desiredFkKeys = new Set();
|
|
841
|
+
for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].sort().join(","));
|
|
842
|
+
for (const [constraintName, columns] of existingByName) {
|
|
843
|
+
const key = [...columns].sort().join(",");
|
|
844
|
+
if (!desiredFkKeys.has(key)) {
|
|
845
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP CONSTRAINT ${qi(constraintName)}`;
|
|
846
|
+
this._log(ddl);
|
|
847
|
+
await this._exec().exec(ddl);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const existingKeys = new Set([...existingByName.values()].map((cols) => cols.sort().join(",")));
|
|
851
|
+
for (const fk of this._table.foreignKeys.values()) {
|
|
852
|
+
const key = [...fk.fields].sort().join(",");
|
|
853
|
+
if (!existingKeys.has(key)) {
|
|
854
|
+
const localCols = fk.fields.map((f) => qi(f)).join(", ");
|
|
855
|
+
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
856
|
+
let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
|
|
857
|
+
if (fk.onDelete) ddl += ` ON DELETE ${(0, __atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
|
|
858
|
+
if (fk.onUpdate) ddl += ` ON UPDATE ${(0, __atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
|
|
859
|
+
this._log(ddl);
|
|
860
|
+
await this._exec().exec(ddl);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async dropForeignKeys(fkFieldKeys) {
|
|
865
|
+
if (fkFieldKeys.length === 0) return;
|
|
866
|
+
const keySet = new Set(fkFieldKeys);
|
|
867
|
+
const existingByName = await this._getExistingFkConstraints();
|
|
868
|
+
for (const [constraintName, cols] of existingByName) {
|
|
869
|
+
const key = cols.sort().join(",");
|
|
870
|
+
if (keySet.has(key)) {
|
|
871
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP CONSTRAINT ${qi(constraintName)}`;
|
|
872
|
+
this._log(ddl);
|
|
873
|
+
await this._exec().exec(ddl);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/** Queries information_schema for existing FK constraints. */ async _getExistingFkConstraints() {
|
|
878
|
+
const rows = await this._exec().all(`SELECT kcu.constraint_name, kcu.column_name
|
|
879
|
+
FROM information_schema.table_constraints tc
|
|
880
|
+
JOIN information_schema.key_column_usage kcu
|
|
881
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
882
|
+
WHERE tc.table_name = $1 AND tc.table_schema = COALESCE($2, 'public')
|
|
883
|
+
AND tc.constraint_type = 'FOREIGN KEY'`, [this._table.tableName, this._schema]);
|
|
884
|
+
const byName = new Map();
|
|
885
|
+
for (const row of rows) {
|
|
886
|
+
let cols = byName.get(row.constraint_name);
|
|
887
|
+
if (!cols) {
|
|
888
|
+
cols = [];
|
|
889
|
+
byName.set(row.constraint_name, cols);
|
|
890
|
+
}
|
|
891
|
+
cols.push(row.column_name);
|
|
892
|
+
}
|
|
893
|
+
return byName;
|
|
894
|
+
}
|
|
895
|
+
getSearchIndexes() {
|
|
896
|
+
const indexes = [];
|
|
897
|
+
for (const index of this._table.indexes.values()) if (index.type === "fulltext") indexes.push({
|
|
898
|
+
name: index.key,
|
|
899
|
+
description: `GIN tsvector index on ${index.fields.map((f) => f.name).join(", ")}`,
|
|
900
|
+
type: "text"
|
|
901
|
+
});
|
|
902
|
+
for (const [field, vec] of this._vectorFields) indexes.push({
|
|
903
|
+
name: vec.indexName,
|
|
904
|
+
description: `vector(${vec.dimensions}) on ${field}, ${vec.similarity}`,
|
|
905
|
+
type: "vector"
|
|
906
|
+
});
|
|
907
|
+
return indexes;
|
|
908
|
+
}
|
|
909
|
+
async search(text, query, indexName) {
|
|
910
|
+
if (!text.trim()) return [];
|
|
911
|
+
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
912
|
+
const { sql, params } = buildSelect(this.resolveTableName(), combinedWhere, query.controls);
|
|
913
|
+
this._log(sql, params);
|
|
914
|
+
return this._exec().all(sql, params);
|
|
915
|
+
}
|
|
916
|
+
async searchWithCount(text, query, indexName) {
|
|
917
|
+
if (!text.trim()) return {
|
|
918
|
+
data: [],
|
|
919
|
+
count: 0
|
|
920
|
+
};
|
|
921
|
+
const combinedWhere = this._buildSearchWhere(text, query, indexName);
|
|
922
|
+
const tableName = this.resolveTableName();
|
|
923
|
+
const selectPromise = (async () => {
|
|
924
|
+
const { sql, params } = buildSelect(tableName, combinedWhere, query.controls);
|
|
925
|
+
this._log(sql, params);
|
|
926
|
+
return this._exec().all(sql, params);
|
|
927
|
+
})();
|
|
928
|
+
const countPromise = (async () => {
|
|
929
|
+
const raw = {
|
|
930
|
+
sql: `SELECT COUNT(*) as cnt FROM ${quoteTableName(tableName)} WHERE ${combinedWhere.sql}`,
|
|
931
|
+
params: combinedWhere.params
|
|
932
|
+
};
|
|
933
|
+
const { sql, params } = (0, __atscript_db_sql_tools.finalizeParams)(pgDialect, raw);
|
|
934
|
+
this._log(sql, params);
|
|
935
|
+
const row = await this._exec().get(sql, params);
|
|
936
|
+
return parseCount(row?.cnt);
|
|
937
|
+
})();
|
|
938
|
+
const [data, count] = await Promise.all([selectPromise, countPromise]);
|
|
939
|
+
return {
|
|
940
|
+
data,
|
|
941
|
+
count
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
_buildSearchWhere(text, query, indexName) {
|
|
945
|
+
const fulltextIndex = this._getFulltextIndex(indexName);
|
|
946
|
+
if (!fulltextIndex) throw new Error("No fulltext index found for search");
|
|
947
|
+
const tsvectorExpr = this._buildTsvectorExpr(fulltextIndex.fields);
|
|
948
|
+
const where = buildWhere(query.filter);
|
|
949
|
+
const tsqueryClause = `to_tsvector('english', ${tsvectorExpr}) @@ plainto_tsquery('english', ?)`;
|
|
950
|
+
return {
|
|
951
|
+
sql: where.sql === "1=1" ? tsqueryClause : `${where.sql} AND ${tsqueryClause}`,
|
|
952
|
+
params: [...where.params, text]
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */ _buildTsvectorExpr(fields) {
|
|
956
|
+
return fields.map((f) => `coalesce(${qi(f.name)}, '')`).join(" || ' ' || ");
|
|
957
|
+
}
|
|
958
|
+
_getFulltextIndex(indexName) {
|
|
959
|
+
for (const index of this._table.indexes.values()) if (index.type === "fulltext") {
|
|
960
|
+
if (!indexName || index.key === indexName) return index;
|
|
961
|
+
}
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Detects pgvector support by attempting to enable the extension.
|
|
966
|
+
* Idempotent — safe to call multiple times.
|
|
967
|
+
*/ async _detectVectorSupport() {
|
|
968
|
+
if (this._supportsVector !== undefined) return this._supportsVector;
|
|
969
|
+
try {
|
|
970
|
+
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS vector");
|
|
971
|
+
this._supportsVector = true;
|
|
972
|
+
} catch {
|
|
973
|
+
this._supportsVector = false;
|
|
974
|
+
}
|
|
975
|
+
return this._supportsVector;
|
|
976
|
+
}
|
|
977
|
+
isVectorSearchable() {
|
|
978
|
+
return this._supportsVector === true && this._vectorFields.size > 0;
|
|
979
|
+
}
|
|
980
|
+
async vectorSearch(vector, query, indexName) {
|
|
981
|
+
await this._detectVectorSupport();
|
|
982
|
+
if (!this._supportsVector) throw new Error("Vector search requires the pgvector extension");
|
|
983
|
+
const { sql, params } = this._buildVectorSearchQuery(vector, query, indexName);
|
|
984
|
+
this._log(sql, params);
|
|
985
|
+
return this._exec().all(sql, params);
|
|
986
|
+
}
|
|
987
|
+
async vectorSearchWithCount(vector, query, indexName) {
|
|
988
|
+
await this._detectVectorSupport();
|
|
989
|
+
if (!this._supportsVector) throw new Error("Vector search requires the pgvector extension");
|
|
990
|
+
const { sql, params } = this._buildVectorSearchQuery(vector, query, indexName);
|
|
991
|
+
const { sql: countSql, params: countParams } = this._buildVectorSearchCountQuery(vector, query, indexName);
|
|
992
|
+
this._log(sql, params);
|
|
993
|
+
this._log(countSql, countParams);
|
|
994
|
+
const [data, countRow] = await Promise.all([this._exec().all(sql, params), this._exec().get(countSql, countParams)]);
|
|
995
|
+
const count = parseCount(countRow?.cnt);
|
|
996
|
+
return {
|
|
997
|
+
data,
|
|
998
|
+
count
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
/** Resolves vector field and computes shared context for vector search SQL builders. */ _prepareVectorSearch(vector, query, indexName) {
|
|
1002
|
+
let field;
|
|
1003
|
+
let vec;
|
|
1004
|
+
if (indexName) {
|
|
1005
|
+
let found = false;
|
|
1006
|
+
for (const [f, v] of this._vectorFields) if (v.indexName === indexName) {
|
|
1007
|
+
field = f;
|
|
1008
|
+
vec = v;
|
|
1009
|
+
found = true;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
if (!found) throw new Error(`Vector index "${indexName}" not found`);
|
|
1013
|
+
} else {
|
|
1014
|
+
const first = this._vectorFields.entries().next();
|
|
1015
|
+
if (first.done) throw new Error("No vector fields defined");
|
|
1016
|
+
field = first.value[0];
|
|
1017
|
+
vec = first.value[1];
|
|
1018
|
+
}
|
|
1019
|
+
const distanceOp = similarityToPgOp(vec.similarity);
|
|
1020
|
+
const where = buildWhere(query.filter);
|
|
1021
|
+
const controls = query.controls || {};
|
|
1022
|
+
const threshold = this._resolveVectorThreshold(controls, vec.indexName);
|
|
1023
|
+
return {
|
|
1024
|
+
field,
|
|
1025
|
+
vec,
|
|
1026
|
+
distanceOp,
|
|
1027
|
+
where,
|
|
1028
|
+
controls,
|
|
1029
|
+
threshold,
|
|
1030
|
+
tableName: this.resolveTableName(),
|
|
1031
|
+
vectorStr: vectorToString(vector)
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
_buildVectorSearchQuery(vector, query, indexName) {
|
|
1035
|
+
const ctx = this._prepareVectorSearch(vector, query, indexName);
|
|
1036
|
+
let inner = `SELECT *, (${qi(ctx.field)} ${ctx.distanceOp} $1::vector) AS _distance FROM ${quoteTableName(ctx.tableName)} WHERE ${offsetPlaceholders((0, __atscript_db_sql_tools.finalizeParams)(pgDialect, ctx.where), 1).sql}`;
|
|
1037
|
+
const params = [ctx.vectorStr, ...ctx.where.params];
|
|
1038
|
+
let sql = `SELECT * FROM (${inner}) _v`;
|
|
1039
|
+
if (ctx.threshold !== undefined) {
|
|
1040
|
+
sql += ` WHERE _distance <= $${params.length + 1}`;
|
|
1041
|
+
params.push(thresholdToDistance(ctx.threshold, ctx.vec.similarity));
|
|
1042
|
+
}
|
|
1043
|
+
sql += ` ORDER BY _distance ASC`;
|
|
1044
|
+
if (ctx.controls.$skip) {
|
|
1045
|
+
sql += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
1046
|
+
params.push(ctx.controls.$limit || 1e3, ctx.controls.$skip);
|
|
1047
|
+
} else {
|
|
1048
|
+
sql += ` LIMIT $${params.length + 1}`;
|
|
1049
|
+
params.push(ctx.controls.$limit || 20);
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
sql,
|
|
1053
|
+
params
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
_buildVectorSearchCountQuery(vector, query, indexName) {
|
|
1057
|
+
const ctx = this._prepareVectorSearch(vector, query, indexName);
|
|
1058
|
+
let inner = `SELECT (${qi(ctx.field)} ${ctx.distanceOp} $1::vector) AS _distance FROM ${quoteTableName(ctx.tableName)} WHERE ${offsetPlaceholders((0, __atscript_db_sql_tools.finalizeParams)(pgDialect, ctx.where), 1).sql}`;
|
|
1059
|
+
const params = [ctx.vectorStr, ...ctx.where.params];
|
|
1060
|
+
let sql = `SELECT COUNT(*) AS cnt FROM (${inner}) _v`;
|
|
1061
|
+
if (ctx.threshold !== undefined) {
|
|
1062
|
+
sql += ` WHERE _distance <= $${params.length + 1}`;
|
|
1063
|
+
params.push(thresholdToDistance(ctx.threshold, ctx.vec.similarity));
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
sql,
|
|
1067
|
+
params
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */ _resolveVectorThreshold(controls, indexName) {
|
|
1071
|
+
const queryThreshold = controls.$threshold;
|
|
1072
|
+
if (queryThreshold !== undefined) return queryThreshold;
|
|
1073
|
+
return this._vectorThresholds.get(indexName);
|
|
1074
|
+
}
|
|
1075
|
+
constructor(driver) {
|
|
1076
|
+
super(), _define_property$1(this, "driver", void 0), _define_property$1(this, "supportsColumnModify", void 0), _define_property$1(this, "_incrementFields", void 0), _define_property$1(this, "_autoIncrementStart", void 0), _define_property$1(this, "_nocaseColumns", void 0), _define_property$1(this, "_citextProvisioned", void 0), _define_property$1(this, "_supportsVector", void 0), _define_property$1(this, "_vectorFields", void 0), _define_property$1(this, "_vectorThresholds", void 0), this.driver = driver, this.supportsColumnModify = true, this._incrementFields = new Set(), this._nocaseColumns = new Set(), this._citextProvisioned = false, this._vectorFields = new Map(), this._vectorThresholds = new Map();
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
_define_property$1(PostgresAdapter, "NATIVE_DEFAULT_FNS", new Set([
|
|
1080
|
+
"now",
|
|
1081
|
+
"uuid",
|
|
1082
|
+
"increment"
|
|
1083
|
+
]));
|
|
1084
|
+
/**
|
|
1085
|
+
* Normalizes PostgreSQL information_schema data_type values
|
|
1086
|
+
* to match the format produced by `pgTypeFromField()`.
|
|
1087
|
+
*/ function normalizePgType(dataType, maxLength, numericPrecision, numericScale, udtName, formattedType) {
|
|
1088
|
+
const dt = dataType.toLowerCase();
|
|
1089
|
+
switch (dt) {
|
|
1090
|
+
case "character varying": return maxLength ? `VARCHAR(${maxLength})` : "VARCHAR(255)";
|
|
1091
|
+
case "character": return maxLength ? `CHAR(${maxLength})` : "CHAR(1)";
|
|
1092
|
+
case "integer": return "INTEGER";
|
|
1093
|
+
case "smallint": return "SMALLINT";
|
|
1094
|
+
case "bigint": return "BIGINT";
|
|
1095
|
+
case "double precision": return "DOUBLE PRECISION";
|
|
1096
|
+
case "numeric": return numericPrecision != null && numericScale != null ? `NUMERIC(${numericPrecision},${numericScale})` : "NUMERIC";
|
|
1097
|
+
case "boolean": return "BOOLEAN";
|
|
1098
|
+
case "text": return "TEXT";
|
|
1099
|
+
case "jsonb": return "JSONB";
|
|
1100
|
+
case "json": return "JSON";
|
|
1101
|
+
case "timestamp with time zone": return "TIMESTAMPTZ";
|
|
1102
|
+
case "timestamp without time zone": return "TIMESTAMP";
|
|
1103
|
+
case "uuid": return "UUID";
|
|
1104
|
+
case "user-defined": {
|
|
1105
|
+
if (udtName === "vector") return formattedType;
|
|
1106
|
+
if (udtName === "citext") return "CITEXT";
|
|
1107
|
+
return udtName?.toUpperCase() ?? "USER-DEFINED";
|
|
1108
|
+
}
|
|
1109
|
+
default: return dataType.toUpperCase();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Normalizes PostgreSQL column_default values to match the format
|
|
1114
|
+
* produced by `serializeDefaultValue()`.
|
|
1115
|
+
*/ function normalizePgDefault(value, isIdentity) {
|
|
1116
|
+
if (isIdentity === "YES") return "fn:increment";
|
|
1117
|
+
if (value === null) return undefined;
|
|
1118
|
+
let cleaned = value;
|
|
1119
|
+
while (cleaned.startsWith("(") && cleaned.endsWith(")")) cleaned = cleaned.slice(1, -1);
|
|
1120
|
+
const lower = cleaned.toLowerCase();
|
|
1121
|
+
if (lower === "true") return "1";
|
|
1122
|
+
if (lower === "false") return "0";
|
|
1123
|
+
if (lower === "current_timestamp" || lower === "now()" || lower.startsWith("current_timestamp::") || lower.startsWith("now()::")) return "fn:now";
|
|
1124
|
+
if (lower.includes("extract") && lower.includes("epoch") && lower.includes("now()")) return "fn:now";
|
|
1125
|
+
if (lower === "gen_random_uuid()") return "fn:uuid";
|
|
1126
|
+
if (lower.startsWith("nextval(")) return "fn:increment";
|
|
1127
|
+
const castMatch = cleaned.match(/^'(.*)'::[\w\s]+$/);
|
|
1128
|
+
if (castMatch) return castMatch[1].replace(/''/g, "'");
|
|
1129
|
+
if (cleaned.startsWith("'") && cleaned.endsWith("'")) return cleaned.slice(1, -1).replace(/''/g, "'");
|
|
1130
|
+
return cleaned;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Converts a normalized similarity threshold (0-1) to a pgvector max distance.
|
|
1134
|
+
*
|
|
1135
|
+
* The threshold is a normalized score matching MongoDB Atlas semantics:
|
|
1136
|
+
* cosine score = (1 + cosine_similarity) / 2, range [0, 1]
|
|
1137
|
+
* pgvector cosine distance = 1 - cosine_similarity, range [0, 2]
|
|
1138
|
+
*
|
|
1139
|
+
* Conversion: distance = 2 * (1 - score)
|
|
1140
|
+
*/ function thresholdToDistance(threshold, similarity) {
|
|
1141
|
+
switch (similarity) {
|
|
1142
|
+
case "euclidean": return threshold;
|
|
1143
|
+
case "dotProduct": return -threshold;
|
|
1144
|
+
default: return 2 * (1 - threshold);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/** Maps generic similarity metric to PostgreSQL distance operator. */ function similarityToPgOp(similarity) {
|
|
1148
|
+
switch (similarity) {
|
|
1149
|
+
case "euclidean": return "<->";
|
|
1150
|
+
case "dotProduct": return "<#>";
|
|
1151
|
+
default: return "<=>";
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
/** Maps generic similarity metric to pgvector index ops class. */ function similarityToPgOps(similarity) {
|
|
1155
|
+
switch (similarity) {
|
|
1156
|
+
case "euclidean": return "vector_l2_ops";
|
|
1157
|
+
case "dotProduct": return "vector_ip_ops";
|
|
1158
|
+
default: return "vector_cosine_ops";
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/** Formats a number[] vector as pgvector input: '[1.0, 2.0, ...]'. */ function vectorToString(vector) {
|
|
1162
|
+
return `[${vector.join(",")}]`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region packages/db-postgres/src/pg-driver.ts
|
|
1167
|
+
function _define_property(obj, key, value) {
|
|
1168
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
1169
|
+
value,
|
|
1170
|
+
enumerable: true,
|
|
1171
|
+
configurable: true,
|
|
1172
|
+
writable: true
|
|
1173
|
+
});
|
|
1174
|
+
else obj[key] = value;
|
|
1175
|
+
return obj;
|
|
1176
|
+
}
|
|
1177
|
+
/** pg rejects `undefined` in bind arrays — coerce to `null`. */ function sanitizeParams(params) {
|
|
1178
|
+
if (!params) return [];
|
|
1179
|
+
return params.map((v) => v === undefined ? null : v);
|
|
1180
|
+
}
|
|
1181
|
+
/** Parses TIMESTAMP/TIMESTAMPTZ to epoch milliseconds. */ function parseTimestamp(val) {
|
|
1182
|
+
const ms = new Date(val).getTime();
|
|
1183
|
+
return Number.isNaN(ms) ? val : ms;
|
|
1184
|
+
}
|
|
1185
|
+
/** Parses NUMERIC to number. */ function parseNumeric(val) {
|
|
1186
|
+
const n = Number.parseFloat(val);
|
|
1187
|
+
return Number.isNaN(n) ? val : n;
|
|
1188
|
+
}
|
|
1189
|
+
/** Parses INT8/BIGINT to number. Returns string if value exceeds safe integer range. */ function parseBigInt(val) {
|
|
1190
|
+
const n = Number.parseInt(val, 10);
|
|
1191
|
+
return Number.isNaN(n) || !Number.isSafeInteger(n) ? val : n;
|
|
1192
|
+
}
|
|
1193
|
+
/** OIDs for types we override. */ const TIMESTAMP_OID = 1114;
|
|
1194
|
+
const TIMESTAMPTZ_OID = 1184;
|
|
1195
|
+
const NUMERIC_OID = 1700;
|
|
1196
|
+
const INT8_OID = 20;
|
|
1197
|
+
/**
|
|
1198
|
+
* Creates a per-pool custom types config that overrides specific parsers
|
|
1199
|
+
* without mutating the global `pg.types`.
|
|
1200
|
+
*
|
|
1201
|
+
* - TIMESTAMP/TIMESTAMPTZ → epoch milliseconds (number)
|
|
1202
|
+
* - NUMERIC → number (not string)
|
|
1203
|
+
* - INT8/BIGINT → number (for JS-safe range)
|
|
1204
|
+
*/ function createCustomTypes(pgTypes) {
|
|
1205
|
+
const overrides = new Map([
|
|
1206
|
+
[TIMESTAMP_OID, parseTimestamp],
|
|
1207
|
+
[TIMESTAMPTZ_OID, parseTimestamp],
|
|
1208
|
+
[NUMERIC_OID, parseNumeric],
|
|
1209
|
+
[INT8_OID, parseBigInt]
|
|
1210
|
+
]);
|
|
1211
|
+
return { getTypeParser(oid, format) {
|
|
1212
|
+
const custom = overrides.get(oid);
|
|
1213
|
+
if (custom) return custom;
|
|
1214
|
+
return pgTypes.getTypeParser(oid, format);
|
|
1215
|
+
} };
|
|
1216
|
+
}
|
|
1217
|
+
var PgDriver = class {
|
|
1218
|
+
getPool() {
|
|
1219
|
+
return this.pool || this.poolInit;
|
|
1220
|
+
}
|
|
1221
|
+
async run(sql, params) {
|
|
1222
|
+
const pool = await this.getPool();
|
|
1223
|
+
const result = await pool.query(sql, sanitizeParams(params));
|
|
1224
|
+
return {
|
|
1225
|
+
affectedRows: result.rowCount ?? 0,
|
|
1226
|
+
rows: result.rows ?? []
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
async all(sql, params) {
|
|
1230
|
+
const pool = await this.getPool();
|
|
1231
|
+
const result = await pool.query(sql, sanitizeParams(params));
|
|
1232
|
+
return result.rows;
|
|
1233
|
+
}
|
|
1234
|
+
async get(sql, params) {
|
|
1235
|
+
const pool = await this.getPool();
|
|
1236
|
+
const result = await pool.query(sql, sanitizeParams(params));
|
|
1237
|
+
return result.rows[0] ?? null;
|
|
1238
|
+
}
|
|
1239
|
+
async exec(sql) {
|
|
1240
|
+
const pool = await this.getPool();
|
|
1241
|
+
await pool.query(sql);
|
|
1242
|
+
}
|
|
1243
|
+
async getConnection() {
|
|
1244
|
+
const pool = await this.getPool();
|
|
1245
|
+
const client = await pool.connect();
|
|
1246
|
+
return {
|
|
1247
|
+
async run(sql, params) {
|
|
1248
|
+
const result = await client.query(sql, sanitizeParams(params));
|
|
1249
|
+
return {
|
|
1250
|
+
affectedRows: result.rowCount ?? 0,
|
|
1251
|
+
rows: result.rows ?? []
|
|
1252
|
+
};
|
|
1253
|
+
},
|
|
1254
|
+
async all(sql, params) {
|
|
1255
|
+
const result = await client.query(sql, sanitizeParams(params));
|
|
1256
|
+
return result.rows;
|
|
1257
|
+
},
|
|
1258
|
+
async get(sql, params) {
|
|
1259
|
+
const result = await client.query(sql, sanitizeParams(params));
|
|
1260
|
+
return result.rows[0] ?? null;
|
|
1261
|
+
},
|
|
1262
|
+
async exec(sql) {
|
|
1263
|
+
await client.query(sql);
|
|
1264
|
+
},
|
|
1265
|
+
release() {
|
|
1266
|
+
client.release();
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
async close() {
|
|
1271
|
+
const pool = await this.getPool();
|
|
1272
|
+
await pool.end();
|
|
1273
|
+
}
|
|
1274
|
+
constructor(poolOrConfig) {
|
|
1275
|
+
_define_property(this, "pool", void 0);
|
|
1276
|
+
_define_property(this, "poolInit", void 0);
|
|
1277
|
+
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
1278
|
+
else this.poolInit = import("pg").then((pg) => {
|
|
1279
|
+
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
1280
|
+
const types = pg.default?.types ?? pg.types;
|
|
1281
|
+
const customTypes = types ? createCustomTypes(types) : undefined;
|
|
1282
|
+
if (typeof poolOrConfig === "string") this.pool = new Pool({
|
|
1283
|
+
connectionString: poolOrConfig,
|
|
1284
|
+
types: customTypes
|
|
1285
|
+
});
|
|
1286
|
+
else this.pool = new Pool({
|
|
1287
|
+
...poolOrConfig,
|
|
1288
|
+
types: customTypes
|
|
1289
|
+
});
|
|
1290
|
+
return this.pool;
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region packages/db-postgres/src/plugin/annotations.ts
|
|
1297
|
+
const annotations = {
|
|
1298
|
+
type: new __atscript_core.AnnotationSpec({
|
|
1299
|
+
description: "Overrides the native PostgreSQL column type.\n\n```atscript\n@db.pg.type \"CITEXT\"\nname: string\n```",
|
|
1300
|
+
nodeType: ["prop"],
|
|
1301
|
+
multiple: false,
|
|
1302
|
+
argument: {
|
|
1303
|
+
name: "type",
|
|
1304
|
+
type: "string",
|
|
1305
|
+
description: "Native PostgreSQL column type (e.g., \"CITEXT\", \"INET\", \"MACADDR\")."
|
|
1306
|
+
}
|
|
1307
|
+
}),
|
|
1308
|
+
schema: new __atscript_core.AnnotationSpec({
|
|
1309
|
+
description: "Specifies the PostgreSQL schema for the table.\n\n**Default:** `\"public\"`\n\n```atscript\n@db.pg.schema \"analytics\"\nexport interface Events { ... }\n```",
|
|
1310
|
+
nodeType: ["interface"],
|
|
1311
|
+
multiple: false,
|
|
1312
|
+
argument: {
|
|
1313
|
+
name: "schema",
|
|
1314
|
+
type: "string",
|
|
1315
|
+
description: "PostgreSQL schema name (e.g., \"public\", \"analytics\")."
|
|
1316
|
+
}
|
|
1317
|
+
}),
|
|
1318
|
+
collate: new __atscript_core.AnnotationSpec({
|
|
1319
|
+
description: "Specifies a native PostgreSQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.pg.collate \"tr-x-icu\"\nname: string\n```",
|
|
1320
|
+
nodeType: ["interface", "prop"],
|
|
1321
|
+
multiple: false,
|
|
1322
|
+
argument: {
|
|
1323
|
+
name: "collation",
|
|
1324
|
+
type: "string",
|
|
1325
|
+
description: "Native PostgreSQL collation name (e.g., \"tr-x-icu\", \"C\", \"und-x-icu\")."
|
|
1326
|
+
}
|
|
1327
|
+
})
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
//#endregion
|
|
1331
|
+
//#region packages/db-postgres/src/plugin/index.ts
|
|
1332
|
+
const PostgresPlugin = () => ({
|
|
1333
|
+
name: "postgres",
|
|
1334
|
+
config() {
|
|
1335
|
+
return { annotations: { db: { pg: annotations } } };
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
//#endregion
|
|
1340
|
+
//#region packages/db-postgres/src/index.ts
|
|
1341
|
+
function createAdapter(uri, options) {
|
|
1342
|
+
const driver = new PgDriver({
|
|
1343
|
+
connectionString: uri,
|
|
1344
|
+
...options
|
|
1345
|
+
});
|
|
1346
|
+
return new __atscript_db.DbSpace(() => new PostgresAdapter(driver));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
//#endregion
|
|
1350
|
+
exports.PgDriver = PgDriver
|
|
1351
|
+
exports.PostgresAdapter = PostgresAdapter
|
|
1352
|
+
exports.PostgresPlugin = PostgresPlugin
|
|
1353
|
+
exports.buildWhere = buildWhere
|
|
1354
|
+
exports.createAdapter = createAdapter
|