@atscript/db-postgres 0.1.38 → 0.1.40
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/README.md +59 -0
- package/dist/index-HFavdjnf.d.mts +6 -0
- package/dist/index-Tl2u3rTo.d.cts +6 -0
- package/dist/index.cjs +308 -305
- package/dist/index.d.cts +279 -0
- package/dist/index.d.mts +279 -0
- package/dist/index.mjs +283 -257
- package/dist/plugin-C4tRUZGB.mjs +53 -0
- package/dist/plugin-Cx6tCzk4.cjs +58 -0
- package/dist/plugin.cjs +6 -72
- package/dist/plugin.d.cts +2 -0
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.mjs +2 -47
- package/package.json +26 -36
- package/LICENSE +0 -21
- package/dist/index.d.ts +0 -278
- package/dist/plugin.d.ts +0 -5
package/dist/index.mjs
CHANGED
|
@@ -1,31 +1,45 @@
|
|
|
1
|
+
import { t as PostgresPlugin } from "./plugin-C4tRUZGB.mjs";
|
|
1
2
|
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
|
|
2
3
|
import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, refActionToSql } from "@atscript/db-sql-tools";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
//#region src/sql-builder.ts
|
|
5
|
+
/**
|
|
6
|
+
* PostgreSQL-aware default value for a given design type.
|
|
7
|
+
* PG uses native BOOLEAN (not TINYINT), so defaults must be `false`/`true` not `0`/`1`.
|
|
8
|
+
*/
|
|
6
9
|
function defaultValueForType$1(designType) {
|
|
7
10
|
if (designType === "boolean") return "false";
|
|
8
11
|
if (designType === "json" || designType === "object") return "'{}'";
|
|
9
12
|
if (designType === "array") return "'[]'";
|
|
10
13
|
return defaultValueForType(designType);
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* PostgreSQL-aware default value literal.
|
|
17
|
+
* Converts boolean defaults to `true`/`false` instead of `1`/`0`.
|
|
18
|
+
*/
|
|
12
19
|
function defaultValueToSqlLiteral$1(designType, value) {
|
|
13
20
|
if (designType === "boolean") return value === "true" || value === "1" ? "true" : "false";
|
|
14
21
|
return defaultValueToSqlLiteral(designType, value);
|
|
15
22
|
}
|
|
23
|
+
/** Escapes a PostgreSQL identifier by doubling double-quotes. */
|
|
16
24
|
function esc(name) {
|
|
17
25
|
return name.replace(/"/g, "\"\"");
|
|
18
26
|
}
|
|
27
|
+
/** Double-quote-quotes a single identifier. */
|
|
19
28
|
function qi(name) {
|
|
20
29
|
return `"${esc(name)}"`;
|
|
21
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Double-quote-quotes a table name, handling `schema.table` format.
|
|
33
|
+
* Input is a raw name like `public.users` or just `users`.
|
|
34
|
+
*/
|
|
22
35
|
function quoteTableName(name) {
|
|
23
36
|
const dot = name.indexOf(".");
|
|
24
37
|
if (dot >= 0) return `${qi(name.slice(0, dot))}.${qi(name.slice(dot + 1))}`;
|
|
25
38
|
return qi(name);
|
|
26
39
|
}
|
|
27
|
-
/** Converts JS values to SQL-bindable params, keeping booleans native. */
|
|
28
|
-
|
|
40
|
+
/** Converts JS values to SQL-bindable params, keeping booleans native. */
|
|
41
|
+
function toPgValue(value) {
|
|
42
|
+
if (value === void 0) return null;
|
|
29
43
|
if (value === null) return null;
|
|
30
44
|
if (value instanceof Date) return value.toISOString();
|
|
31
45
|
if (typeof value === "object") return JSON.stringify(value);
|
|
@@ -41,7 +55,7 @@ const pgDialect = {
|
|
|
41
55
|
unlimitedLimit: "ALL",
|
|
42
56
|
toValue: toPgValue,
|
|
43
57
|
toParam(value) {
|
|
44
|
-
if (value ===
|
|
58
|
+
if (value === void 0) return null;
|
|
45
59
|
return value;
|
|
46
60
|
},
|
|
47
61
|
regex(quotedCol, value) {
|
|
@@ -62,8 +76,8 @@ function buildInsert$1(table, data) {
|
|
|
62
76
|
function buildSelect$1(table, where, controls) {
|
|
63
77
|
return buildSelect(pgDialect, table, where, controls);
|
|
64
78
|
}
|
|
65
|
-
function buildUpdate$1(table, data, where, limit) {
|
|
66
|
-
return buildUpdate(pgDialect, table, data, where, limit);
|
|
79
|
+
function buildUpdate$1(table, data, where, limit, ops) {
|
|
80
|
+
return buildUpdate(pgDialect, table, data, where, limit, ops);
|
|
67
81
|
}
|
|
68
82
|
function buildDelete$1(table, where, limit) {
|
|
69
83
|
return buildDelete(pgDialect, table, where, limit);
|
|
@@ -77,6 +91,13 @@ function buildAggregateSelect$1(table, where, controls) {
|
|
|
77
91
|
function buildAggregateCount$1(table, where, controls) {
|
|
78
92
|
return buildAggregateCount(pgDialect, table, where, controls);
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Maps portable collation values to PostgreSQL collation names.
|
|
96
|
+
*
|
|
97
|
+
* Returns `null` for `'nocase'` — case-insensitive columns use CITEXT type instead
|
|
98
|
+
* (provisioned via `CREATE EXTENSION IF NOT EXISTS citext` in `ensureTable`).
|
|
99
|
+
* Users can also opt into ICU collation explicitly via `@db.pg.collate "und-u-ks-level2"`.
|
|
100
|
+
*/
|
|
80
101
|
function collationToPg(collation) {
|
|
81
102
|
switch (collation) {
|
|
82
103
|
case "binary": return "\"C\"";
|
|
@@ -90,7 +111,8 @@ function collationToPg(collation) {
|
|
|
90
111
|
* PostgreSQL only has signed integer types:
|
|
91
112
|
* uint16 (0-65535) → INTEGER (not SMALLINT which caps at 32767)
|
|
92
113
|
* uint32 (0-4.3B) → BIGINT (not INTEGER which caps at ~2.1B)
|
|
93
|
-
*/
|
|
114
|
+
*/
|
|
115
|
+
function intTypeFromTags(tags) {
|
|
94
116
|
if (tags?.has("int8") || tags?.has("byte")) return "SMALLINT";
|
|
95
117
|
if (tags?.has("uint8")) return "SMALLINT";
|
|
96
118
|
if (tags?.has("int16") || tags?.has("port")) return "SMALLINT";
|
|
@@ -100,6 +122,12 @@ function collationToPg(collation) {
|
|
|
100
122
|
if (tags?.has("int64") || tags?.has("uint64")) return "BIGINT";
|
|
101
123
|
return "INTEGER";
|
|
102
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Maps an Atscript field descriptor to a PostgreSQL column type.
|
|
127
|
+
*
|
|
128
|
+
* For FK fields, delegates to the target PK's type via `field.fkTargetField`
|
|
129
|
+
* so the FK column type always matches the referenced column.
|
|
130
|
+
*/
|
|
103
131
|
function pgTypeFromField(field) {
|
|
104
132
|
if (field.fkTargetField) return pgTypeFromField(field.fkTargetField);
|
|
105
133
|
const tags = field.type?.type?.tags;
|
|
@@ -108,36 +136,36 @@ function pgTypeFromField(field) {
|
|
|
108
136
|
if (pgTypeOverride) return pgTypeOverride;
|
|
109
137
|
const precision = metadata?.get("db.column.precision");
|
|
110
138
|
switch (field.designType) {
|
|
111
|
-
case "number":
|
|
139
|
+
case "number":
|
|
112
140
|
if (precision) return `NUMERIC(${precision.precision},${precision.scale})`;
|
|
113
141
|
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") return "BIGINT";
|
|
114
142
|
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "now") return "BIGINT";
|
|
115
143
|
if (tags?.has("int")) return intTypeFromTags(tags);
|
|
116
144
|
return "DOUBLE PRECISION";
|
|
117
|
-
}
|
|
118
145
|
case "integer": return intTypeFromTags(tags);
|
|
119
|
-
case "decimal":
|
|
146
|
+
case "decimal":
|
|
120
147
|
if (precision) return `NUMERIC(${precision.precision},${precision.scale})`;
|
|
121
148
|
return "NUMERIC(10,2)";
|
|
122
|
-
}
|
|
123
149
|
case "boolean": return "BOOLEAN";
|
|
124
150
|
case "string": {
|
|
125
151
|
if (field.collate === "nocase" && !metadata?.get("db.pg.collate")) return "CITEXT";
|
|
126
152
|
if (tags?.has("char")) return "CHAR(1)";
|
|
127
|
-
const maxLen = metadata?.get("expect.maxLength")?.length;
|
|
128
|
-
if (maxLen !==
|
|
153
|
+
const maxLen = (metadata?.get("expect.maxLength"))?.length;
|
|
154
|
+
if (maxLen !== void 0) return `VARCHAR(${maxLen})`;
|
|
129
155
|
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
130
156
|
return "TEXT";
|
|
131
157
|
}
|
|
132
158
|
case "json":
|
|
133
159
|
case "object":
|
|
134
160
|
case "array": return "JSONB";
|
|
135
|
-
default:
|
|
161
|
+
default:
|
|
136
162
|
if (field.isPrimaryKey || field.defaultValue) return "VARCHAR(255)";
|
|
137
163
|
return "TEXT";
|
|
138
|
-
}
|
|
139
164
|
}
|
|
140
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Builds a CREATE TABLE IF NOT EXISTS statement with PostgreSQL syntax.
|
|
168
|
+
*/
|
|
141
169
|
function buildCreateTable(table, fields, foreignKeys, options) {
|
|
142
170
|
const colDefs = [];
|
|
143
171
|
const primaryKeys = fields.filter((f) => f.isPrimaryKey);
|
|
@@ -147,17 +175,17 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
147
175
|
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
148
176
|
if (options?.incrementFields?.has(field.physicalName)) {
|
|
149
177
|
const start = options.autoIncrementStart;
|
|
150
|
-
def += start !==
|
|
178
|
+
def += start !== void 0 ? ` GENERATED BY DEFAULT AS IDENTITY (START WITH ${start})` : " GENERATED BY DEFAULT AS IDENTITY";
|
|
151
179
|
}
|
|
152
180
|
if (!field.optional && !field.isPrimaryKey && !options?.incrementFields?.has(field.physicalName)) def += " NOT NULL";
|
|
153
181
|
if (field.defaultValue?.kind === "value") def += ` DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`;
|
|
154
|
-
else if (field.defaultValue?.kind === "fn") {
|
|
182
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
155
183
|
if (field.defaultValue.fn === "uuid") def += " DEFAULT gen_random_uuid()";
|
|
156
|
-
else if (field.defaultValue.fn === "now") def += " DEFAULT (extract(epoch from now()) * 1000)::bigint";
|
|
184
|
+
else if (field.defaultValue.fn === "now") def += " DEFAULT (extract(epoch from now()) * 1000)::bigint";
|
|
157
185
|
}
|
|
158
186
|
const nativeCollate = field.type?.metadata?.get("db.pg.collate");
|
|
159
187
|
if (nativeCollate) def += ` COLLATE "${nativeCollate}"`;
|
|
160
|
-
else if (field.collate) {
|
|
188
|
+
else if (field.collate) {
|
|
161
189
|
const pgCollate = collationToPg(field.collate);
|
|
162
190
|
if (pgCollate) def += ` COLLATE ${pgCollate}`;
|
|
163
191
|
}
|
|
@@ -183,41 +211,82 @@ else if (field.collate) {
|
|
|
183
211
|
}
|
|
184
212
|
return `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${colDefs.join(", ")})`;
|
|
185
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Offsets numbered placeholders ($1, $2, ...) in a SQL fragment by a given amount.
|
|
216
|
+
* Used when WHERE params follow SET params in UPDATE statements.
|
|
217
|
+
*/
|
|
186
218
|
function offsetPlaceholders(fragment, offset) {
|
|
187
219
|
if (offset === 0) return fragment;
|
|
188
|
-
const sql = fragment.sql.replace(/\$(\d+)/g, (_, n) => `$${Number(n) + offset}`);
|
|
189
220
|
return {
|
|
190
|
-
sql,
|
|
221
|
+
sql: fragment.sql.replace(/\$(\d+)/g, (_, n) => `$${Number(n) + offset}`),
|
|
191
222
|
params: fragment.params
|
|
192
223
|
};
|
|
193
224
|
}
|
|
194
|
-
|
|
195
225
|
//#endregion
|
|
196
|
-
//#region
|
|
226
|
+
//#region src/filter-builder.ts
|
|
227
|
+
/**
|
|
228
|
+
* Translates a filter expression into a parameterized PostgreSQL WHERE clause.
|
|
229
|
+
*
|
|
230
|
+
* Note: The returned fragment uses `?` placeholders (not `$N`).
|
|
231
|
+
* Finalization to `$N` happens when the fragment is consumed by a DML builder
|
|
232
|
+
* (buildSelect, buildUpdate, etc.) via the dialect's `paramPlaceholder`.
|
|
233
|
+
*
|
|
234
|
+
* Case-insensitive columns (`@db.collate 'nocase'`) are handled by CITEXT
|
|
235
|
+
* column type at the storage level — no query-side wrapping needed.
|
|
236
|
+
*/
|
|
197
237
|
function buildWhere(filter) {
|
|
198
238
|
return buildWhere$1(pgDialect, filter);
|
|
199
239
|
}
|
|
200
|
-
|
|
201
240
|
//#endregion
|
|
202
|
-
//#region
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
value,
|
|
206
|
-
enumerable: true,
|
|
207
|
-
configurable: true,
|
|
208
|
-
writable: true
|
|
209
|
-
});
|
|
210
|
-
else obj[key] = value;
|
|
211
|
-
return obj;
|
|
212
|
-
}
|
|
213
|
-
/** PostgreSQL COUNT() may return string (bigint) — parse to number. */ function parseCount(value) {
|
|
241
|
+
//#region src/postgres-adapter.ts
|
|
242
|
+
/** PostgreSQL COUNT() may return string (bigint) — parse to number. */
|
|
243
|
+
function parseCount(value) {
|
|
214
244
|
if (typeof value === "string") return Number.parseInt(value, 10);
|
|
215
245
|
return value ?? 0;
|
|
216
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* PostgreSQL adapter for {@link AtscriptDbTable}.
|
|
249
|
+
*
|
|
250
|
+
* Accepts any {@link TPgDriver} implementation — the actual PostgreSQL driver
|
|
251
|
+
* is fully swappable (pg Pool, custom implementations, etc.).
|
|
252
|
+
*
|
|
253
|
+
* Usage:
|
|
254
|
+
* ```typescript
|
|
255
|
+
* import { PgDriver, PostgresAdapter } from '@atscript/db-postgres'
|
|
256
|
+
* import { DbSpace } from '@atscript/db'
|
|
257
|
+
*
|
|
258
|
+
* const driver = new PgDriver('postgresql://user@localhost:5432/mydb')
|
|
259
|
+
* const space = new DbSpace(() => new PostgresAdapter(driver))
|
|
260
|
+
* const users = space.getTable(UsersType)
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
217
263
|
var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
218
|
-
|
|
264
|
+
supportsColumnModify = true;
|
|
265
|
+
static NATIVE_DEFAULT_FNS = new Set([
|
|
266
|
+
"now",
|
|
267
|
+
"uuid",
|
|
268
|
+
"increment"
|
|
269
|
+
]);
|
|
270
|
+
_incrementFields = /* @__PURE__ */ new Set();
|
|
271
|
+
_autoIncrementStart;
|
|
272
|
+
/** Physical column names with @db.collate 'nocase'. Used to trigger CITEXT extension. */
|
|
273
|
+
_nocaseColumns = /* @__PURE__ */ new Set();
|
|
274
|
+
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
275
|
+
_citextProvisioned = false;
|
|
276
|
+
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
277
|
+
_supportsVector;
|
|
278
|
+
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
279
|
+
_vectorFields = /* @__PURE__ */ new Map();
|
|
280
|
+
/** Default similarity thresholds per vector field (from @db.search.vector.threshold). */
|
|
281
|
+
_vectorThresholds = /* @__PURE__ */ new Map();
|
|
282
|
+
/** Schema name for queries (null falls through to 'public'). */
|
|
283
|
+
get _schema() {
|
|
219
284
|
return this._table.schema ?? null;
|
|
220
285
|
}
|
|
286
|
+
constructor(driver) {
|
|
287
|
+
super();
|
|
288
|
+
this.driver = driver;
|
|
289
|
+
}
|
|
221
290
|
async _beginTransaction() {
|
|
222
291
|
const conn = await this.driver.getConnection();
|
|
223
292
|
try {
|
|
@@ -250,11 +319,12 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
250
319
|
/**
|
|
251
320
|
* Returns the active executor: dedicated connection if inside a transaction,
|
|
252
321
|
* otherwise the pool-based driver.
|
|
253
|
-
*/
|
|
254
|
-
|
|
255
|
-
return
|
|
322
|
+
*/
|
|
323
|
+
_exec() {
|
|
324
|
+
return this._getTransactionState() ?? this.driver;
|
|
256
325
|
}
|
|
257
|
-
/** PostgreSQL enforces FK constraints natively. */
|
|
326
|
+
/** PostgreSQL enforces FK constraints natively. */
|
|
327
|
+
supportsNativeForeignKeys() {
|
|
258
328
|
return true;
|
|
259
329
|
}
|
|
260
330
|
prepareId(id, _fieldType) {
|
|
@@ -285,7 +355,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
285
355
|
indexName
|
|
286
356
|
});
|
|
287
357
|
const threshold = metadata.get("db.search.vector.threshold");
|
|
288
|
-
if (threshold !==
|
|
358
|
+
if (threshold !== void 0) this._vectorThresholds.set(indexName, threshold);
|
|
289
359
|
}
|
|
290
360
|
}
|
|
291
361
|
getDesiredTableOptions() {
|
|
@@ -298,8 +368,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
298
368
|
* Converts vector fields between JavaScript `number[]` and pgvector text format `[1,2,3]`.
|
|
299
369
|
* The pg driver serializes JS arrays as PostgreSQL array literals `{1,2,3}` which is
|
|
300
370
|
* invalid for the pgvector `vector` type — it expects bracket-delimited `[1,2,3]`.
|
|
301
|
-
*/
|
|
302
|
-
|
|
371
|
+
*/
|
|
372
|
+
formatValue(field) {
|
|
373
|
+
if (!this._vectorFields.has(field.path)) return;
|
|
303
374
|
return {
|
|
304
375
|
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
305
376
|
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
@@ -312,29 +383,24 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
312
383
|
* PostgreSQL uses SQLSTATE codes:
|
|
313
384
|
* - 23505 = unique_violation
|
|
314
385
|
* - 23503 = foreign_key_violation
|
|
315
|
-
*/
|
|
386
|
+
*/
|
|
387
|
+
async _wrapConstraintError(fn) {
|
|
316
388
|
try {
|
|
317
389
|
return await fn();
|
|
318
390
|
} catch (error) {
|
|
319
391
|
if (error && typeof error === "object" && "code" in error) {
|
|
320
392
|
const err = error;
|
|
321
|
-
if (err.code === "23505") {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}]);
|
|
327
|
-
}
|
|
328
|
-
if (err.code === "23503") {
|
|
329
|
-
const errors = this._mapFkError(err.detail ?? err.message, err.constraint);
|
|
330
|
-
throw new DbError("FK_VIOLATION", errors);
|
|
331
|
-
}
|
|
393
|
+
if (err.code === "23505") throw new DbError("CONFLICT", [{
|
|
394
|
+
path: this._extractFieldFromConstraint(err.constraint) ?? "",
|
|
395
|
+
message: err.detail ?? err.message
|
|
396
|
+
}]);
|
|
397
|
+
if (err.code === "23503") throw new DbError("FK_VIOLATION", this._mapFkError(err.detail ?? err.message, err.constraint));
|
|
332
398
|
}
|
|
333
399
|
throw error;
|
|
334
400
|
}
|
|
335
401
|
}
|
|
336
402
|
_extractFieldFromConstraint(constraint) {
|
|
337
|
-
if (!constraint) return
|
|
403
|
+
if (!constraint) return;
|
|
338
404
|
const tableName = this._table.tableName;
|
|
339
405
|
if (constraint.startsWith(`${tableName}_`) && constraint.endsWith("_key")) {
|
|
340
406
|
const fieldPart = constraint.slice(tableName.length + 1, -4);
|
|
@@ -347,9 +413,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
347
413
|
const fkMatch = detail.match(/Key \(([^)]+)\)/);
|
|
348
414
|
if (fkMatch) {
|
|
349
415
|
const physicalCol = fkMatch[1].split(",")[0].trim();
|
|
350
|
-
const field = this._table.fieldDescriptors.find((f) => f.physicalName === physicalCol);
|
|
351
416
|
return [{
|
|
352
|
-
path:
|
|
417
|
+
path: this._table.fieldDescriptors.find((f) => f.physicalName === physicalCol)?.path ?? physicalCol,
|
|
353
418
|
message: detail
|
|
354
419
|
}];
|
|
355
420
|
}
|
|
@@ -363,9 +428,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
363
428
|
const pkCols = this._table.primaryKeys.map((pk) => qi(pk));
|
|
364
429
|
if (pkCols.length > 0) sql += ` RETURNING ${pkCols.join(", ")}`;
|
|
365
430
|
this._log(sql, params);
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
return { insertedId: this._resolveInsertedId(data, returned ? Object.values(returned)[0] : undefined) };
|
|
431
|
+
const returned = (await this._wrapConstraintError(() => this._exec().run(sql, params))).rows?.[0];
|
|
432
|
+
return { insertedId: this._resolveInsertedId(data, returned ? Object.values(returned)[0] : void 0) };
|
|
369
433
|
}
|
|
370
434
|
async insertMany(data) {
|
|
371
435
|
if (data.length === 0) return {
|
|
@@ -400,7 +464,7 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
400
464
|
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
401
465
|
for (let i = 0; i < batchSize; i++) {
|
|
402
466
|
const returned = result.rows?.[i];
|
|
403
|
-
allIds.push(this._resolveInsertedId(data[offset + i], returned ? Object.values(returned)[0] :
|
|
467
|
+
allIds.push(this._resolveInsertedId(data[offset + i], returned ? Object.values(returned)[0] : void 0));
|
|
404
468
|
}
|
|
405
469
|
}
|
|
406
470
|
return {
|
|
@@ -427,40 +491,32 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
427
491
|
}
|
|
428
492
|
async count(query) {
|
|
429
493
|
const where = buildWhere(query.filter);
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
sql: `SELECT COUNT(*) as cnt FROM ${quoteTableName(tableName)} WHERE ${where.sql}`,
|
|
494
|
+
const { sql, params } = finalizeParams(pgDialect, {
|
|
495
|
+
sql: `SELECT COUNT(*) as cnt FROM ${quoteTableName(this.resolveTableName())} WHERE ${where.sql}`,
|
|
433
496
|
params: where.params
|
|
434
|
-
};
|
|
435
|
-
const { sql, params } = finalizeParams(pgDialect, raw);
|
|
497
|
+
});
|
|
436
498
|
this._log(sql, params);
|
|
437
|
-
|
|
438
|
-
return parseCount(row?.cnt);
|
|
499
|
+
return parseCount((await this._exec().get(sql, params))?.cnt);
|
|
439
500
|
}
|
|
440
501
|
async aggregate(query) {
|
|
441
502
|
const where = buildWhere(query.filter);
|
|
442
503
|
const tableName = this.resolveTableName();
|
|
443
504
|
if (query.controls.$count) {
|
|
444
|
-
const { sql
|
|
445
|
-
this._log(sql
|
|
446
|
-
|
|
447
|
-
const count = parseCount(row?.count);
|
|
448
|
-
return [{ count }];
|
|
505
|
+
const { sql, params } = buildAggregateCount$1(tableName, where, query.controls);
|
|
506
|
+
this._log(sql, params);
|
|
507
|
+
return [{ count: parseCount((await this._exec().get(sql, params))?.count) }];
|
|
449
508
|
}
|
|
450
509
|
const { sql, params } = buildAggregateSelect$1(tableName, where, query.controls);
|
|
451
510
|
this._log(sql, params);
|
|
452
511
|
return this._exec().all(sql, params);
|
|
453
512
|
}
|
|
454
|
-
async updateOne(filter, data) {
|
|
513
|
+
async updateOne(filter, data, ops) {
|
|
455
514
|
const where = buildWhere(filter);
|
|
456
515
|
const tableName = this.resolveTableName();
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const offsetWhere = offsetPlaceholders(finalizedWhere, keys.length);
|
|
462
|
-
const sql = `UPDATE ${quoteTableName(tableName)} SET ${setClauses.join(", ")} WHERE ctid = (SELECT ctid FROM ${quoteTableName(tableName)} WHERE ${offsetWhere.sql} LIMIT 1)`;
|
|
463
|
-
const params = [...setParams, ...where.params];
|
|
516
|
+
const { sql, params } = buildUpdate$1(tableName, data, {
|
|
517
|
+
sql: `ctid = (SELECT ctid FROM ${quoteTableName(tableName)} WHERE ${where.sql} LIMIT 1)`,
|
|
518
|
+
params: where.params
|
|
519
|
+
}, void 0, ops);
|
|
464
520
|
this._log(sql, params);
|
|
465
521
|
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
466
522
|
return {
|
|
@@ -468,9 +524,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
468
524
|
modifiedCount: result.affectedRows
|
|
469
525
|
};
|
|
470
526
|
}
|
|
471
|
-
async updateMany(filter, data) {
|
|
527
|
+
async updateMany(filter, data, ops) {
|
|
472
528
|
const where = buildWhere(filter);
|
|
473
|
-
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where);
|
|
529
|
+
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, ops);
|
|
474
530
|
this._log(sql, params);
|
|
475
531
|
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
476
532
|
return {
|
|
@@ -487,21 +543,18 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
487
543
|
async deleteOne(filter) {
|
|
488
544
|
const where = buildWhere(filter);
|
|
489
545
|
const tableName = this.resolveTableName();
|
|
490
|
-
const
|
|
546
|
+
const { sql, params } = finalizeParams(pgDialect, {
|
|
491
547
|
sql: `DELETE FROM ${quoteTableName(tableName)} WHERE ctid = (SELECT ctid FROM ${quoteTableName(tableName)} WHERE ${where.sql} LIMIT 1)`,
|
|
492
548
|
params: where.params
|
|
493
|
-
};
|
|
494
|
-
const { sql, params } = finalizeParams(pgDialect, raw);
|
|
549
|
+
});
|
|
495
550
|
this._log(sql, params);
|
|
496
|
-
|
|
497
|
-
return { deletedCount: result.affectedRows };
|
|
551
|
+
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
498
552
|
}
|
|
499
553
|
async deleteMany(filter) {
|
|
500
554
|
const where = buildWhere(filter);
|
|
501
555
|
const { sql, params } = buildDelete$1(this.resolveTableName(), where);
|
|
502
556
|
this._log(sql, params);
|
|
503
|
-
|
|
504
|
-
return { deletedCount: result.affectedRows };
|
|
557
|
+
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
505
558
|
}
|
|
506
559
|
async ensureTable() {
|
|
507
560
|
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
@@ -509,9 +562,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
509
562
|
this._citextProvisioned = true;
|
|
510
563
|
} catch (err) {
|
|
511
564
|
const msg = err instanceof Error ? err.message : String(err);
|
|
512
|
-
throw new Error(`Failed to create citext extension for @db.collate 'nocase' columns: ${msg}.
|
|
565
|
+
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.`, { cause: err });
|
|
513
566
|
}
|
|
514
|
-
if (this._supportsVector ===
|
|
567
|
+
if (this._supportsVector === void 0 && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
515
568
|
if (this._table instanceof AtscriptDbView) return this._ensureView();
|
|
516
569
|
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
517
570
|
incrementFields: this._incrementFields,
|
|
@@ -569,18 +622,18 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
569
622
|
const sqlType = this.typeMapper(field);
|
|
570
623
|
let ddl = `ALTER TABLE ${quoteTableName(tableName)} ADD COLUMN ${qi(field.physicalName)} ${sqlType}`;
|
|
571
624
|
if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment") ddl += " GENERATED BY DEFAULT AS IDENTITY";
|
|
572
|
-
else {
|
|
625
|
+
else {
|
|
573
626
|
if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
|
|
574
627
|
if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`;
|
|
575
|
-
else if (field.defaultValue?.kind === "fn") {
|
|
628
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
576
629
|
if (field.defaultValue.fn === "uuid") ddl += " DEFAULT gen_random_uuid()";
|
|
577
|
-
else if (field.defaultValue.fn === "now") ddl += " DEFAULT (extract(epoch from now()) * 1000)::bigint";
|
|
630
|
+
else if (field.defaultValue.fn === "now") ddl += " DEFAULT (extract(epoch from now()) * 1000)::bigint";
|
|
578
631
|
} else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType$1(field.designType)}`;
|
|
579
632
|
}
|
|
580
633
|
if (field.collate) {
|
|
581
634
|
const nativeCollate = field.type?.metadata?.get("db.pg.collate");
|
|
582
635
|
if (nativeCollate) ddl += ` COLLATE "${nativeCollate}"`;
|
|
583
|
-
else {
|
|
636
|
+
else {
|
|
584
637
|
const pgCollate = collationToPg(field.collate);
|
|
585
638
|
if (pgCollate) ddl += ` COLLATE ${pgCollate}`;
|
|
586
639
|
}
|
|
@@ -611,7 +664,7 @@ else {
|
|
|
611
664
|
for (const { field } of diff.defaultChanged ?? []) {
|
|
612
665
|
let ddl;
|
|
613
666
|
if (field.defaultValue?.kind === "value") ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${defaultValueToSqlLiteral$1(field.designType, field.defaultValue.value)}`;
|
|
614
|
-
else if (field.defaultValue?.kind === "fn") {
|
|
667
|
+
else if (field.defaultValue?.kind === "fn") {
|
|
615
668
|
const fnExpr = field.defaultValue.fn === "now" ? "(extract(epoch from now()) * 1000)::bigint" : field.defaultValue.fn === "uuid" ? "gen_random_uuid()" : `${field.defaultValue.fn}()`;
|
|
616
669
|
ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} SET DEFAULT ${fnExpr}`;
|
|
617
670
|
} else ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${qi(field.physicalName)} DROP DEFAULT`;
|
|
@@ -647,7 +700,7 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
647
700
|
SELECT constraint_name FROM information_schema.table_constraints
|
|
648
701
|
WHERE table_name = $2 AND table_schema = COALESCE($1, 'public') AND constraint_type = 'PRIMARY KEY'
|
|
649
702
|
)`, [schema, this._table.tableName]);
|
|
650
|
-
const fkByName = new Map();
|
|
703
|
+
const fkByName = /* @__PURE__ */ new Map();
|
|
651
704
|
for (const fk of fkRefs) {
|
|
652
705
|
let entry = fkByName.get(fk.constraint_name);
|
|
653
706
|
if (!entry) {
|
|
@@ -726,7 +779,8 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
726
779
|
* value doesn't conflict with existing data. PostgreSQL's GENERATED BY DEFAULT
|
|
727
780
|
* AS IDENTITY does not advance the sequence when rows are inserted with explicit
|
|
728
781
|
* values, so this is needed after data seeding, bulk imports, or recreateTable().
|
|
729
|
-
*/
|
|
782
|
+
*/
|
|
783
|
+
async _resetIdentitySequences() {
|
|
730
784
|
if (this._incrementFields.size === 0) return;
|
|
731
785
|
const tableName = this.resolveTableName();
|
|
732
786
|
const emptyFallback = this._autoIncrementStart ?? 1;
|
|
@@ -738,8 +792,7 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
738
792
|
}
|
|
739
793
|
}
|
|
740
794
|
async tableExists() {
|
|
741
|
-
|
|
742
|
-
return row?.exists ?? false;
|
|
795
|
+
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;
|
|
743
796
|
}
|
|
744
797
|
async dropTable() {
|
|
745
798
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(this.resolveTableName())} CASCADE`;
|
|
@@ -785,9 +838,9 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
785
838
|
createIndex: async (index) => {
|
|
786
839
|
if (index.type === "fulltext") {
|
|
787
840
|
const tsvectorExpr = this._buildTsvectorExpr(index.fields);
|
|
788
|
-
const sql
|
|
789
|
-
this._log(sql
|
|
790
|
-
await this._exec().exec(sql
|
|
841
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gin(to_tsvector('english', ${tsvectorExpr}))`;
|
|
842
|
+
this._log(sql);
|
|
843
|
+
await this._exec().exec(sql);
|
|
791
844
|
return;
|
|
792
845
|
}
|
|
793
846
|
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
@@ -797,8 +850,7 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
797
850
|
await this._exec().exec(sql);
|
|
798
851
|
},
|
|
799
852
|
dropIndex: async (name) => {
|
|
800
|
-
const
|
|
801
|
-
const sql = `DROP INDEX IF EXISTS ${schemaPrefix}${qi(name)}`;
|
|
853
|
+
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
802
854
|
this._log(sql);
|
|
803
855
|
await this._exec().exec(sql);
|
|
804
856
|
}
|
|
@@ -813,19 +865,19 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
813
865
|
}
|
|
814
866
|
async syncForeignKeys() {
|
|
815
867
|
const existingByName = await this._getExistingFkConstraints();
|
|
816
|
-
const desiredFkKeys = new Set();
|
|
817
|
-
for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].
|
|
868
|
+
const desiredFkKeys = /* @__PURE__ */ new Set();
|
|
869
|
+
for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].toSorted().join(","));
|
|
818
870
|
for (const [constraintName, columns] of existingByName) {
|
|
819
|
-
const key = [...columns].
|
|
871
|
+
const key = [...columns].toSorted().join(",");
|
|
820
872
|
if (!desiredFkKeys.has(key)) {
|
|
821
873
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP CONSTRAINT ${qi(constraintName)}`;
|
|
822
874
|
this._log(ddl);
|
|
823
875
|
await this._exec().exec(ddl);
|
|
824
876
|
}
|
|
825
877
|
}
|
|
826
|
-
const existingKeys = new Set([...existingByName.values()].map((cols) => cols.
|
|
878
|
+
const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
|
|
827
879
|
for (const fk of this._table.foreignKeys.values()) {
|
|
828
|
-
const key = [...fk.fields].
|
|
880
|
+
const key = [...fk.fields].toSorted().join(",");
|
|
829
881
|
if (!existingKeys.has(key)) {
|
|
830
882
|
const localCols = fk.fields.map((f) => qi(f)).join(", ");
|
|
831
883
|
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
@@ -842,7 +894,7 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
842
894
|
const keySet = new Set(fkFieldKeys);
|
|
843
895
|
const existingByName = await this._getExistingFkConstraints();
|
|
844
896
|
for (const [constraintName, cols] of existingByName) {
|
|
845
|
-
const key = cols.
|
|
897
|
+
const key = cols.toSorted().join(",");
|
|
846
898
|
if (keySet.has(key)) {
|
|
847
899
|
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP CONSTRAINT ${qi(constraintName)}`;
|
|
848
900
|
this._log(ddl);
|
|
@@ -850,14 +902,15 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
850
902
|
}
|
|
851
903
|
}
|
|
852
904
|
}
|
|
853
|
-
/** Queries information_schema for existing FK constraints. */
|
|
905
|
+
/** Queries information_schema for existing FK constraints. */
|
|
906
|
+
async _getExistingFkConstraints() {
|
|
854
907
|
const rows = await this._exec().all(`SELECT kcu.constraint_name, kcu.column_name
|
|
855
908
|
FROM information_schema.table_constraints tc
|
|
856
909
|
JOIN information_schema.key_column_usage kcu
|
|
857
910
|
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
858
911
|
WHERE tc.table_name = $1 AND tc.table_schema = COALESCE($2, 'public')
|
|
859
912
|
AND tc.constraint_type = 'FOREIGN KEY'`, [this._table.tableName, this._schema]);
|
|
860
|
-
const byName = new Map();
|
|
913
|
+
const byName = /* @__PURE__ */ new Map();
|
|
861
914
|
for (const row of rows) {
|
|
862
915
|
let cols = byName.get(row.constraint_name);
|
|
863
916
|
if (!cols) {
|
|
@@ -902,14 +955,12 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
902
955
|
return this._exec().all(sql, params);
|
|
903
956
|
})();
|
|
904
957
|
const countPromise = (async () => {
|
|
905
|
-
const
|
|
958
|
+
const { sql, params } = finalizeParams(pgDialect, {
|
|
906
959
|
sql: `SELECT COUNT(*) as cnt FROM ${quoteTableName(tableName)} WHERE ${combinedWhere.sql}`,
|
|
907
960
|
params: combinedWhere.params
|
|
908
|
-
};
|
|
909
|
-
const { sql, params } = finalizeParams(pgDialect, raw);
|
|
961
|
+
});
|
|
910
962
|
this._log(sql, params);
|
|
911
|
-
|
|
912
|
-
return parseCount(row?.cnt);
|
|
963
|
+
return parseCount((await this._exec().get(sql, params))?.cnt);
|
|
913
964
|
})();
|
|
914
965
|
const [data, count] = await Promise.all([selectPromise, countPromise]);
|
|
915
966
|
return {
|
|
@@ -928,20 +979,21 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
928
979
|
params: [...where.params, text]
|
|
929
980
|
};
|
|
930
981
|
}
|
|
931
|
-
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
982
|
+
/** Builds the tsvector SQL expression for a fulltext index's fields. Must match between index DDL and queries. */
|
|
983
|
+
_buildTsvectorExpr(fields) {
|
|
932
984
|
return fields.map((f) => `coalesce(${qi(f.name)}, '')`).join(" || ' ' || ");
|
|
933
985
|
}
|
|
934
986
|
_getFulltextIndex(indexName) {
|
|
935
987
|
for (const index of this._table.indexes.values()) if (index.type === "fulltext") {
|
|
936
988
|
if (!indexName || index.key === indexName) return index;
|
|
937
989
|
}
|
|
938
|
-
return undefined;
|
|
939
990
|
}
|
|
940
991
|
/**
|
|
941
992
|
* Detects pgvector support by attempting to enable the extension.
|
|
942
993
|
* Idempotent — safe to call multiple times.
|
|
943
|
-
*/
|
|
944
|
-
|
|
994
|
+
*/
|
|
995
|
+
async _detectVectorSupport() {
|
|
996
|
+
if (this._supportsVector !== void 0) return this._supportsVector;
|
|
945
997
|
try {
|
|
946
998
|
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS vector");
|
|
947
999
|
this._supportsVector = true;
|
|
@@ -968,13 +1020,13 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
968
1020
|
this._log(sql, params);
|
|
969
1021
|
this._log(countSql, countParams);
|
|
970
1022
|
const [data, countRow] = await Promise.all([this._exec().all(sql, params), this._exec().get(countSql, countParams)]);
|
|
971
|
-
const count = parseCount(countRow?.cnt);
|
|
972
1023
|
return {
|
|
973
1024
|
data,
|
|
974
|
-
count
|
|
1025
|
+
count: parseCount(countRow?.cnt)
|
|
975
1026
|
};
|
|
976
1027
|
}
|
|
977
|
-
/** Resolves vector field and computes shared context for vector search SQL builders. */
|
|
1028
|
+
/** Resolves vector field and computes shared context for vector search SQL builders. */
|
|
1029
|
+
_prepareVectorSearch(vector, query, indexName) {
|
|
978
1030
|
let field;
|
|
979
1031
|
let vec;
|
|
980
1032
|
if (indexName) {
|
|
@@ -1012,7 +1064,7 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
1012
1064
|
let inner = `SELECT *, (${qi(ctx.field)} ${ctx.distanceOp} $1::vector) AS _distance FROM ${quoteTableName(ctx.tableName)} WHERE ${offsetPlaceholders(finalizeParams(pgDialect, ctx.where), 1).sql}`;
|
|
1013
1065
|
const params = [ctx.vectorStr, ...ctx.where.params];
|
|
1014
1066
|
let sql = `SELECT * FROM (${inner}) _v`;
|
|
1015
|
-
if (ctx.threshold !==
|
|
1067
|
+
if (ctx.threshold !== void 0) {
|
|
1016
1068
|
sql += ` WHERE _distance <= $${params.length + 1}`;
|
|
1017
1069
|
params.push(thresholdToDistance(ctx.threshold, ctx.vec.similarity));
|
|
1018
1070
|
}
|
|
@@ -1034,7 +1086,7 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
1034
1086
|
let inner = `SELECT (${qi(ctx.field)} ${ctx.distanceOp} $1::vector) AS _distance FROM ${quoteTableName(ctx.tableName)} WHERE ${offsetPlaceholders(finalizeParams(pgDialect, ctx.where), 1).sql}`;
|
|
1035
1087
|
const params = [ctx.vectorStr, ...ctx.where.params];
|
|
1036
1088
|
let sql = `SELECT COUNT(*) AS cnt FROM (${inner}) _v`;
|
|
1037
|
-
if (ctx.threshold !==
|
|
1089
|
+
if (ctx.threshold !== void 0) {
|
|
1038
1090
|
sql += ` WHERE _distance <= $${params.length + 1}`;
|
|
1039
1091
|
params.push(thresholdToDistance(ctx.threshold, ctx.vec.similarity));
|
|
1040
1092
|
}
|
|
@@ -1043,26 +1095,19 @@ else if (field.defaultValue?.kind === "fn") {
|
|
|
1043
1095
|
params
|
|
1044
1096
|
};
|
|
1045
1097
|
}
|
|
1046
|
-
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
1098
|
+
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
1099
|
+
_resolveVectorThreshold(controls, indexName) {
|
|
1047
1100
|
const queryThreshold = controls.$threshold;
|
|
1048
|
-
if (queryThreshold !==
|
|
1101
|
+
if (queryThreshold !== void 0) return queryThreshold;
|
|
1049
1102
|
return this._vectorThresholds.get(indexName);
|
|
1050
1103
|
}
|
|
1051
|
-
constructor(driver) {
|
|
1052
|
-
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();
|
|
1053
|
-
}
|
|
1054
1104
|
};
|
|
1055
|
-
_define_property$1(PostgresAdapter, "NATIVE_DEFAULT_FNS", new Set([
|
|
1056
|
-
"now",
|
|
1057
|
-
"uuid",
|
|
1058
|
-
"increment"
|
|
1059
|
-
]));
|
|
1060
1105
|
/**
|
|
1061
1106
|
* Normalizes PostgreSQL information_schema data_type values
|
|
1062
1107
|
* to match the format produced by `pgTypeFromField()`.
|
|
1063
|
-
*/
|
|
1064
|
-
|
|
1065
|
-
switch (
|
|
1108
|
+
*/
|
|
1109
|
+
function normalizePgType(dataType, maxLength, numericPrecision, numericScale, udtName, formattedType) {
|
|
1110
|
+
switch (dataType.toLowerCase()) {
|
|
1066
1111
|
case "character varying": return maxLength ? `VARCHAR(${maxLength})` : "VARCHAR(255)";
|
|
1067
1112
|
case "character": return maxLength ? `CHAR(${maxLength})` : "CHAR(1)";
|
|
1068
1113
|
case "integer": return "INTEGER";
|
|
@@ -1077,20 +1122,20 @@ _define_property$1(PostgresAdapter, "NATIVE_DEFAULT_FNS", new Set([
|
|
|
1077
1122
|
case "timestamp with time zone": return "TIMESTAMPTZ";
|
|
1078
1123
|
case "timestamp without time zone": return "TIMESTAMP";
|
|
1079
1124
|
case "uuid": return "UUID";
|
|
1080
|
-
case "user-defined":
|
|
1125
|
+
case "user-defined":
|
|
1081
1126
|
if (udtName === "vector") return formattedType;
|
|
1082
1127
|
if (udtName === "citext") return "CITEXT";
|
|
1083
1128
|
return udtName?.toUpperCase() ?? "USER-DEFINED";
|
|
1084
|
-
}
|
|
1085
1129
|
default: return dataType.toUpperCase();
|
|
1086
1130
|
}
|
|
1087
1131
|
}
|
|
1088
1132
|
/**
|
|
1089
1133
|
* Normalizes PostgreSQL column_default values to match the format
|
|
1090
1134
|
* produced by `serializeDefaultValue()`.
|
|
1091
|
-
*/
|
|
1135
|
+
*/
|
|
1136
|
+
function normalizePgDefault(value, isIdentity) {
|
|
1092
1137
|
if (isIdentity === "YES") return "fn:increment";
|
|
1093
|
-
if (value === null) return
|
|
1138
|
+
if (value === null) return;
|
|
1094
1139
|
let cleaned = value;
|
|
1095
1140
|
while (cleaned.startsWith("(") && cleaned.endsWith(")")) cleaned = cleaned.slice(1, -1);
|
|
1096
1141
|
const lower = cleaned.toLowerCase();
|
|
@@ -1113,60 +1158,58 @@ _define_property$1(PostgresAdapter, "NATIVE_DEFAULT_FNS", new Set([
|
|
|
1113
1158
|
* pgvector cosine distance = 1 - cosine_similarity, range [0, 2]
|
|
1114
1159
|
*
|
|
1115
1160
|
* Conversion: distance = 2 * (1 - score)
|
|
1116
|
-
*/
|
|
1161
|
+
*/
|
|
1162
|
+
function thresholdToDistance(threshold, similarity) {
|
|
1117
1163
|
switch (similarity) {
|
|
1118
1164
|
case "euclidean": return threshold;
|
|
1119
1165
|
case "dotProduct": return -threshold;
|
|
1120
1166
|
default: return 2 * (1 - threshold);
|
|
1121
1167
|
}
|
|
1122
1168
|
}
|
|
1123
|
-
/** Maps generic similarity metric to PostgreSQL distance operator. */
|
|
1169
|
+
/** Maps generic similarity metric to PostgreSQL distance operator. */
|
|
1170
|
+
function similarityToPgOp(similarity) {
|
|
1124
1171
|
switch (similarity) {
|
|
1125
1172
|
case "euclidean": return "<->";
|
|
1126
1173
|
case "dotProduct": return "<#>";
|
|
1127
1174
|
default: return "<=>";
|
|
1128
1175
|
}
|
|
1129
1176
|
}
|
|
1130
|
-
/** Maps generic similarity metric to pgvector index ops class. */
|
|
1177
|
+
/** Maps generic similarity metric to pgvector index ops class. */
|
|
1178
|
+
function similarityToPgOps(similarity) {
|
|
1131
1179
|
switch (similarity) {
|
|
1132
1180
|
case "euclidean": return "vector_l2_ops";
|
|
1133
1181
|
case "dotProduct": return "vector_ip_ops";
|
|
1134
1182
|
default: return "vector_cosine_ops";
|
|
1135
1183
|
}
|
|
1136
1184
|
}
|
|
1137
|
-
/** Formats a number[] vector as pgvector input: '[1.0, 2.0, ...]'. */
|
|
1185
|
+
/** Formats a number[] vector as pgvector input: '[1.0, 2.0, ...]'. */
|
|
1186
|
+
function vectorToString(vector) {
|
|
1138
1187
|
return `[${vector.join(",")}]`;
|
|
1139
1188
|
}
|
|
1140
|
-
|
|
1141
1189
|
//#endregion
|
|
1142
|
-
//#region
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
value,
|
|
1146
|
-
enumerable: true,
|
|
1147
|
-
configurable: true,
|
|
1148
|
-
writable: true
|
|
1149
|
-
});
|
|
1150
|
-
else obj[key] = value;
|
|
1151
|
-
return obj;
|
|
1152
|
-
}
|
|
1153
|
-
/** pg rejects `undefined` in bind arrays — coerce to `null`. */ function sanitizeParams(params) {
|
|
1190
|
+
//#region src/pg-driver.ts
|
|
1191
|
+
/** pg rejects `undefined` in bind arrays — coerce to `null`. */
|
|
1192
|
+
function sanitizeParams(params) {
|
|
1154
1193
|
if (!params) return [];
|
|
1155
|
-
return params.map((v) => v ===
|
|
1194
|
+
return params.map((v) => v === void 0 ? null : v);
|
|
1156
1195
|
}
|
|
1157
|
-
/** Parses TIMESTAMP/TIMESTAMPTZ to epoch milliseconds. */
|
|
1196
|
+
/** Parses TIMESTAMP/TIMESTAMPTZ to epoch milliseconds. */
|
|
1197
|
+
function parseTimestamp(val) {
|
|
1158
1198
|
const ms = new Date(val).getTime();
|
|
1159
1199
|
return Number.isNaN(ms) ? val : ms;
|
|
1160
1200
|
}
|
|
1161
|
-
/** Parses NUMERIC to number. */
|
|
1201
|
+
/** Parses NUMERIC to number. */
|
|
1202
|
+
function parseNumeric(val) {
|
|
1162
1203
|
const n = Number.parseFloat(val);
|
|
1163
1204
|
return Number.isNaN(n) ? val : n;
|
|
1164
1205
|
}
|
|
1165
|
-
/** Parses INT8/BIGINT to number. Returns string if value exceeds safe integer range. */
|
|
1206
|
+
/** Parses INT8/BIGINT to number. Returns string if value exceeds safe integer range. */
|
|
1207
|
+
function parseBigInt(val) {
|
|
1166
1208
|
const n = Number.parseInt(val, 10);
|
|
1167
1209
|
return Number.isNaN(n) || !Number.isSafeInteger(n) ? val : n;
|
|
1168
1210
|
}
|
|
1169
|
-
/** OIDs for types we override. */
|
|
1211
|
+
/** OIDs for types we override. */
|
|
1212
|
+
const TIMESTAMP_OID = 1114;
|
|
1170
1213
|
const TIMESTAMPTZ_OID = 1184;
|
|
1171
1214
|
const NUMERIC_OID = 1700;
|
|
1172
1215
|
const INT8_OID = 20;
|
|
@@ -1177,7 +1220,8 @@ const INT8_OID = 20;
|
|
|
1177
1220
|
* - TIMESTAMP/TIMESTAMPTZ → epoch milliseconds (number)
|
|
1178
1221
|
* - NUMERIC → number (not string)
|
|
1179
1222
|
* - INT8/BIGINT → number (for JS-safe range)
|
|
1180
|
-
*/
|
|
1223
|
+
*/
|
|
1224
|
+
function createCustomTypes(pgTypes) {
|
|
1181
1225
|
const overrides = new Map([
|
|
1182
1226
|
[TIMESTAMP_OID, parseTimestamp],
|
|
1183
1227
|
[TIMESTAMPTZ_OID, parseTimestamp],
|
|
@@ -1190,35 +1234,78 @@ const INT8_OID = 20;
|
|
|
1190
1234
|
return pgTypes.getTypeParser(oid, format);
|
|
1191
1235
|
} };
|
|
1192
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* {@link TPgDriver} implementation backed by `pg` (node-postgres).
|
|
1239
|
+
*
|
|
1240
|
+
* Accepts a connection URI string, a `pg.PoolConfig` object, or a pre-created
|
|
1241
|
+
* `pg.Pool` instance.
|
|
1242
|
+
*
|
|
1243
|
+
* ```typescript
|
|
1244
|
+
* import { PgDriver } from '@atscript/db-postgres'
|
|
1245
|
+
*
|
|
1246
|
+
* // Connection URI
|
|
1247
|
+
* const driver = new PgDriver('postgresql://user:pass@localhost:5432/mydb')
|
|
1248
|
+
*
|
|
1249
|
+
* // Pool options
|
|
1250
|
+
* const driver = new PgDriver({
|
|
1251
|
+
* host: 'localhost',
|
|
1252
|
+
* user: 'postgres',
|
|
1253
|
+
* database: 'mydb',
|
|
1254
|
+
* max: 10,
|
|
1255
|
+
* })
|
|
1256
|
+
*
|
|
1257
|
+
* // Pre-created pool
|
|
1258
|
+
* import pg from 'pg'
|
|
1259
|
+
* const pool = new pg.Pool({ connectionString: '...' })
|
|
1260
|
+
* const driver = new PgDriver(pool)
|
|
1261
|
+
* ```
|
|
1262
|
+
*
|
|
1263
|
+
* Requires `pg` to be installed:
|
|
1264
|
+
* ```bash
|
|
1265
|
+
* pnpm add pg
|
|
1266
|
+
* ```
|
|
1267
|
+
*/
|
|
1193
1268
|
var PgDriver = class {
|
|
1269
|
+
pool;
|
|
1270
|
+
poolInit;
|
|
1271
|
+
constructor(poolOrConfig) {
|
|
1272
|
+
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
1273
|
+
else this.poolInit = import("pg").then((pg) => {
|
|
1274
|
+
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
1275
|
+
const types = pg.default?.types ?? pg.types;
|
|
1276
|
+
const customTypes = types ? createCustomTypes(types) : void 0;
|
|
1277
|
+
if (typeof poolOrConfig === "string") this.pool = new Pool({
|
|
1278
|
+
connectionString: poolOrConfig,
|
|
1279
|
+
types: customTypes
|
|
1280
|
+
});
|
|
1281
|
+
else this.pool = new Pool({
|
|
1282
|
+
...poolOrConfig,
|
|
1283
|
+
types: customTypes
|
|
1284
|
+
});
|
|
1285
|
+
return this.pool;
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1194
1288
|
getPool() {
|
|
1195
1289
|
return this.pool || this.poolInit;
|
|
1196
1290
|
}
|
|
1197
1291
|
async run(sql, params) {
|
|
1198
|
-
const
|
|
1199
|
-
const result = await pool.query(sql, sanitizeParams(params));
|
|
1292
|
+
const result = await (await this.getPool()).query(sql, sanitizeParams(params));
|
|
1200
1293
|
return {
|
|
1201
1294
|
affectedRows: result.rowCount ?? 0,
|
|
1202
1295
|
rows: result.rows ?? []
|
|
1203
1296
|
};
|
|
1204
1297
|
}
|
|
1205
1298
|
async all(sql, params) {
|
|
1206
|
-
|
|
1207
|
-
const result = await pool.query(sql, sanitizeParams(params));
|
|
1208
|
-
return result.rows;
|
|
1299
|
+
return (await (await this.getPool()).query(sql, sanitizeParams(params))).rows;
|
|
1209
1300
|
}
|
|
1210
1301
|
async get(sql, params) {
|
|
1211
|
-
|
|
1212
|
-
const result = await pool.query(sql, sanitizeParams(params));
|
|
1213
|
-
return result.rows[0] ?? null;
|
|
1302
|
+
return (await (await this.getPool()).query(sql, sanitizeParams(params))).rows[0] ?? null;
|
|
1214
1303
|
}
|
|
1215
1304
|
async exec(sql) {
|
|
1216
|
-
|
|
1217
|
-
await pool.query(sql);
|
|
1305
|
+
await (await this.getPool()).query(sql);
|
|
1218
1306
|
}
|
|
1219
1307
|
async getConnection() {
|
|
1220
|
-
const
|
|
1221
|
-
const client = await pool.connect();
|
|
1308
|
+
const client = await (await this.getPool()).connect();
|
|
1222
1309
|
return {
|
|
1223
1310
|
async run(sql, params) {
|
|
1224
1311
|
const result = await client.query(sql, sanitizeParams(params));
|
|
@@ -1228,12 +1315,10 @@ var PgDriver = class {
|
|
|
1228
1315
|
};
|
|
1229
1316
|
},
|
|
1230
1317
|
async all(sql, params) {
|
|
1231
|
-
|
|
1232
|
-
return result.rows;
|
|
1318
|
+
return (await client.query(sql, sanitizeParams(params))).rows;
|
|
1233
1319
|
},
|
|
1234
1320
|
async get(sql, params) {
|
|
1235
|
-
|
|
1236
|
-
return result.rows[0] ?? null;
|
|
1321
|
+
return (await client.query(sql, sanitizeParams(params))).rows[0] ?? null;
|
|
1237
1322
|
},
|
|
1238
1323
|
async exec(sql) {
|
|
1239
1324
|
await client.query(sql);
|
|
@@ -1244,76 +1329,18 @@ var PgDriver = class {
|
|
|
1244
1329
|
};
|
|
1245
1330
|
}
|
|
1246
1331
|
async close() {
|
|
1247
|
-
|
|
1248
|
-
await pool.end();
|
|
1249
|
-
}
|
|
1250
|
-
constructor(poolOrConfig) {
|
|
1251
|
-
_define_property(this, "pool", void 0);
|
|
1252
|
-
_define_property(this, "poolInit", void 0);
|
|
1253
|
-
if (typeof poolOrConfig === "object" && typeof poolOrConfig.query === "function") this.pool = poolOrConfig;
|
|
1254
|
-
else this.poolInit = import("pg").then((pg) => {
|
|
1255
|
-
const Pool = pg.default?.Pool ?? pg.Pool;
|
|
1256
|
-
const types = pg.default?.types ?? pg.types;
|
|
1257
|
-
const customTypes = types ? createCustomTypes(types) : undefined;
|
|
1258
|
-
if (typeof poolOrConfig === "string") this.pool = new Pool({
|
|
1259
|
-
connectionString: poolOrConfig,
|
|
1260
|
-
types: customTypes
|
|
1261
|
-
});
|
|
1262
|
-
else this.pool = new Pool({
|
|
1263
|
-
...poolOrConfig,
|
|
1264
|
-
types: customTypes
|
|
1265
|
-
});
|
|
1266
|
-
return this.pool;
|
|
1267
|
-
});
|
|
1332
|
+
await (await this.getPool()).end();
|
|
1268
1333
|
}
|
|
1269
1334
|
};
|
|
1270
|
-
|
|
1271
1335
|
//#endregion
|
|
1272
|
-
//#region
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
type: "string",
|
|
1281
|
-
description: "Native PostgreSQL column type (e.g., \"CITEXT\", \"INET\", \"MACADDR\")."
|
|
1282
|
-
}
|
|
1283
|
-
}),
|
|
1284
|
-
schema: new AnnotationSpec({
|
|
1285
|
-
description: "Specifies the PostgreSQL schema for the table.\n\n**Default:** `\"public\"`\n\n```atscript\n@db.pg.schema \"analytics\"\nexport interface Events { ... }\n```",
|
|
1286
|
-
nodeType: ["interface"],
|
|
1287
|
-
multiple: false,
|
|
1288
|
-
argument: {
|
|
1289
|
-
name: "schema",
|
|
1290
|
-
type: "string",
|
|
1291
|
-
description: "PostgreSQL schema name (e.g., \"public\", \"analytics\")."
|
|
1292
|
-
}
|
|
1293
|
-
}),
|
|
1294
|
-
collate: new AnnotationSpec({
|
|
1295
|
-
description: "Specifies a native PostgreSQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.pg.collate \"tr-x-icu\"\nname: string\n```",
|
|
1296
|
-
nodeType: ["interface", "prop"],
|
|
1297
|
-
multiple: false,
|
|
1298
|
-
argument: {
|
|
1299
|
-
name: "collation",
|
|
1300
|
-
type: "string",
|
|
1301
|
-
description: "Native PostgreSQL collation name (e.g., \"tr-x-icu\", \"C\", \"und-x-icu\")."
|
|
1302
|
-
}
|
|
1303
|
-
})
|
|
1304
|
-
};
|
|
1305
|
-
|
|
1306
|
-
//#endregion
|
|
1307
|
-
//#region packages/db-postgres/src/plugin/index.ts
|
|
1308
|
-
const PostgresPlugin = () => ({
|
|
1309
|
-
name: "postgres",
|
|
1310
|
-
config() {
|
|
1311
|
-
return { annotations: { db: { pg: annotations } } };
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
|
|
1315
|
-
//#endregion
|
|
1316
|
-
//#region packages/db-postgres/src/index.ts
|
|
1336
|
+
//#region src/index.ts
|
|
1337
|
+
/**
|
|
1338
|
+
* Creates a {@link DbSpace} backed by a PostgreSQL connection pool.
|
|
1339
|
+
*
|
|
1340
|
+
* @param uri - PostgreSQL connection URI (e.g., `postgresql://user@localhost:5432/mydb`)
|
|
1341
|
+
* @param options - Additional pool options passed to pg.
|
|
1342
|
+
* @returns A `DbSpace` that creates `PostgresAdapter` instances per table.
|
|
1343
|
+
*/
|
|
1317
1344
|
function createAdapter(uri, options) {
|
|
1318
1345
|
const driver = new PgDriver({
|
|
1319
1346
|
connectionString: uri,
|
|
@@ -1321,6 +1348,5 @@ function createAdapter(uri, options) {
|
|
|
1321
1348
|
});
|
|
1322
1349
|
return new DbSpace(() => new PostgresAdapter(driver));
|
|
1323
1350
|
}
|
|
1324
|
-
|
|
1325
1351
|
//#endregion
|
|
1326
|
-
export { PgDriver, PostgresAdapter, PostgresPlugin, buildWhere, createAdapter };
|
|
1352
|
+
export { PgDriver, PostgresAdapter, PostgresPlugin, buildWhere, createAdapter };
|