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