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