@atscript/db-postgres 0.1.105 → 0.1.106
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 +177 -10
- package/dist/index.d.cts +20 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +178 -11
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -65,11 +65,50 @@ const pgDialect = {
|
|
|
65
65
|
params: [pattern]
|
|
66
66
|
};
|
|
67
67
|
},
|
|
68
|
+
geoWithin(quotedCol, circle) {
|
|
69
|
+
return {
|
|
70
|
+
sql: `ST_DWithin(${quotedCol}, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)`,
|
|
71
|
+
params: [
|
|
72
|
+
circle.center[0],
|
|
73
|
+
circle.center[1],
|
|
74
|
+
circle.radius
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
},
|
|
68
78
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
69
79
|
paramPlaceholder(index) {
|
|
70
80
|
return `$${index}`;
|
|
71
81
|
}
|
|
72
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Distance-in-meters expression from a geography column to a query point.
|
|
85
|
+
* Used as the `distExpr` of the shared geo search builder.
|
|
86
|
+
*/
|
|
87
|
+
function pgGeoDistanceExpr(quotedCol, point) {
|
|
88
|
+
return {
|
|
89
|
+
sql: `ST_Distance(${quotedCol}, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography)`,
|
|
90
|
+
params: [point[0], point[1]]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Formats a `[lng, lat]` tuple as EWKT — the text input format for geography params. */
|
|
94
|
+
function geoPointToEwkt(point) {
|
|
95
|
+
return `SRID=4326;POINT(${point[0]} ${point[1]})`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parses a hex-encoded (E)WKB point — the raw wire format the pg driver
|
|
99
|
+
* returns for geography columns — back to a `[lng, lat]` tuple.
|
|
100
|
+
* Returns `undefined` for anything that isn't a WKB point.
|
|
101
|
+
*/
|
|
102
|
+
function parseEwkbPointHex(hex) {
|
|
103
|
+
if (hex.length < 42 || !/^[0-9A-Fa-f]+$/.test(hex)) return;
|
|
104
|
+
const buf = Buffer.from(hex, "hex");
|
|
105
|
+
const littleEndian = buf.readUInt8(0) === 1;
|
|
106
|
+
const type = littleEndian ? buf.readUInt32LE(1) : buf.readUInt32BE(1);
|
|
107
|
+
if ((type & 255) !== 1) return;
|
|
108
|
+
const offset = (type & 536870912) !== 0 ? 9 : 5;
|
|
109
|
+
if (buf.length < offset + 16) return;
|
|
110
|
+
return [littleEndian ? buf.readDoubleLE(offset) : buf.readDoubleBE(offset), littleEndian ? buf.readDoubleLE(offset + 8) : buf.readDoubleBE(offset + 8)];
|
|
111
|
+
}
|
|
73
112
|
function buildInsert(table, data) {
|
|
74
113
|
return (0, _atscript_db_sql_tools.buildInsert)(pgDialect, table, data);
|
|
75
114
|
}
|
|
@@ -274,6 +313,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
274
313
|
_nocaseColumns = /* @__PURE__ */ new Set();
|
|
275
314
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
276
315
|
_citextProvisioned = false;
|
|
316
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
317
|
+
_supportsGeo;
|
|
277
318
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
278
319
|
_supportsVector;
|
|
279
320
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -371,11 +412,24 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
371
412
|
* invalid for the pgvector `vector` type — it expects bracket-delimited `[1,2,3]`.
|
|
372
413
|
*/
|
|
373
414
|
formatValue(field) {
|
|
374
|
-
if (
|
|
375
|
-
return {
|
|
415
|
+
if (this._vectorFields.has(field.path)) return {
|
|
376
416
|
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
377
417
|
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
378
418
|
};
|
|
419
|
+
if (field.isGeoPoint && !field.encrypted) return {
|
|
420
|
+
toStorage: (value) => {
|
|
421
|
+
if (!this._supportsGeo) return value;
|
|
422
|
+
const point = (0, _atscript_db_sql_tools.normalizeGeoPointValue)(value);
|
|
423
|
+
return point ? geoPointToEwkt(point) : value;
|
|
424
|
+
},
|
|
425
|
+
fromStorage: (value) => {
|
|
426
|
+
if (typeof value === "string") {
|
|
427
|
+
const point = parseEwkbPointHex(value);
|
|
428
|
+
if (point) return point;
|
|
429
|
+
}
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
379
433
|
}
|
|
380
434
|
/**
|
|
381
435
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -564,6 +618,14 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
564
618
|
this._log(sql, params);
|
|
565
619
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
566
620
|
}
|
|
621
|
+
async prepareTypeMapper() {
|
|
622
|
+
if (this._supportsVector === void 0 && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
623
|
+
if (this._supportsGeo === void 0 && this._hasGeoPointFields()) await this._detectGeoSupport();
|
|
624
|
+
}
|
|
625
|
+
/** Whether the table declares any `db.geoPoint` fields (unencrypted). */
|
|
626
|
+
_hasGeoPointFields() {
|
|
627
|
+
return this._table.fieldDescriptors.some((fd) => fd.isGeoPoint && !fd.encrypted);
|
|
628
|
+
}
|
|
567
629
|
async ensureTable() {
|
|
568
630
|
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
569
631
|
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
|
|
@@ -572,7 +634,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
572
634
|
const msg = err instanceof Error ? err.message : String(err);
|
|
573
635
|
throw new Error(`Failed to create citext extension for @db.collate 'nocase' columns: ${msg}. Either run 'CREATE EXTENSION citext' as a superuser, or use @db.pg.type "CITEXT" after provisioning the extension manually.`, { cause: err });
|
|
574
636
|
}
|
|
575
|
-
|
|
637
|
+
await this.prepareTypeMapper();
|
|
638
|
+
if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
|
|
576
639
|
if (this._table instanceof _atscript_db.AtscriptDbView) return this._ensureView();
|
|
577
640
|
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
578
641
|
incrementFields: this._incrementFields,
|
|
@@ -653,7 +716,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
653
716
|
for (const { field } of diff.typeChanged ?? []) {
|
|
654
717
|
const sqlType = this.typeMapper(field);
|
|
655
718
|
const col = qi(field.physicalName);
|
|
656
|
-
|
|
719
|
+
let ddl;
|
|
720
|
+
if (field.isGeoPoint && sqlType.startsWith("geography")) ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING CASE WHEN ${col} IS NULL THEN NULL ELSE ST_SetSRID(ST_MakePoint((${col}->>0)::float8, (${col}->>1)::float8), 4326)::geography END`;
|
|
721
|
+
else ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${col}::text::${sqlType}`;
|
|
657
722
|
this._log(ddl);
|
|
658
723
|
await this._exec().exec(ddl);
|
|
659
724
|
}
|
|
@@ -814,6 +879,27 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
814
879
|
this._log(ddl);
|
|
815
880
|
await this._exec().exec(ddl);
|
|
816
881
|
}
|
|
882
|
+
async dropIndexesForColumns(columns) {
|
|
883
|
+
const tableName = this._table.tableName;
|
|
884
|
+
const schema = this._schema;
|
|
885
|
+
const rows = await this._exec().all(`SELECT DISTINCT i.relname AS name
|
|
886
|
+
FROM pg_index x
|
|
887
|
+
JOIN pg_class t ON t.oid = x.indrelid
|
|
888
|
+
JOIN pg_class i ON i.oid = x.indexrelid
|
|
889
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
890
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(x.indkey)
|
|
891
|
+
WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')
|
|
892
|
+
AND i.relname LIKE 'atscript__%' AND a.attname = ANY($3)`, [
|
|
893
|
+
tableName,
|
|
894
|
+
schema,
|
|
895
|
+
columns
|
|
896
|
+
]);
|
|
897
|
+
for (const row of rows) {
|
|
898
|
+
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(row.name)}`;
|
|
899
|
+
this._log(sql);
|
|
900
|
+
await this._exec().exec(sql);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
817
903
|
async dropTableByName(tableName) {
|
|
818
904
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
|
|
819
905
|
this._log(ddl);
|
|
@@ -836,14 +922,29 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
836
922
|
const vec = this._vectorFields.get(field.path);
|
|
837
923
|
return this._supportsVector ? `vector(${vec.dimensions})` : "JSONB";
|
|
838
924
|
}
|
|
925
|
+
if (field.isGeoPoint) return this._supportsGeo ? "geography(Point,4326)" : pgTypeFromField(field);
|
|
839
926
|
return pgTypeFromField(field);
|
|
840
927
|
}
|
|
841
928
|
async syncIndexes() {
|
|
842
929
|
const tableName = this._table.tableName;
|
|
843
930
|
const schema = this._schema;
|
|
931
|
+
await this.prepareTypeMapper();
|
|
844
932
|
await this.syncIndexesWithDiff({
|
|
845
|
-
listExisting: async () =>
|
|
846
|
-
|
|
933
|
+
listExisting: async () => {
|
|
934
|
+
return (await this._exec().all(`SELECT i.relname AS name,
|
|
935
|
+
(SELECT array_agg(a.attname::text ORDER BY k.ord)
|
|
936
|
+
FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
937
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
938
|
+
WHERE k.attnum > 0) AS columns
|
|
939
|
+
FROM pg_index x
|
|
940
|
+
JOIN pg_class t ON t.oid = x.indrelid
|
|
941
|
+
JOIN pg_class i ON i.oid = x.indexrelid
|
|
942
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
943
|
+
WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [tableName, schema])).map((r) => ({
|
|
944
|
+
name: r.name,
|
|
945
|
+
columns: r.columns ?? void 0
|
|
946
|
+
}));
|
|
947
|
+
},
|
|
847
948
|
createIndex: async (index) => {
|
|
848
949
|
if (index.type === "fulltext") {
|
|
849
950
|
const tsvectorExpr = this._buildTsvectorExpr(index.fields);
|
|
@@ -852,6 +953,16 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
852
953
|
await this._exec().exec(sql);
|
|
853
954
|
return;
|
|
854
955
|
}
|
|
956
|
+
if (index.type === "geo") {
|
|
957
|
+
if (!this._supportsGeo) {
|
|
958
|
+
this.logger.warn(`[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gist (${qi(index.fields[0].name)})`;
|
|
962
|
+
this._log(sql);
|
|
963
|
+
await this._exec().exec(sql);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
855
966
|
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
856
967
|
const cols = index.fields.map((f) => `${qi(f.name)} ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
857
968
|
const sql = `CREATE ${unique}INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${cols})`;
|
|
@@ -862,10 +973,6 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
862
973
|
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
863
974
|
this._log(sql);
|
|
864
975
|
await this._exec().exec(sql);
|
|
865
|
-
},
|
|
866
|
-
warnUnsupportedTypes: {
|
|
867
|
-
adapter: "postgres",
|
|
868
|
-
types: ["geo"]
|
|
869
976
|
}
|
|
870
977
|
});
|
|
871
978
|
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
@@ -1114,6 +1221,65 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1114
1221
|
if (queryThreshold !== void 0) return queryThreshold;
|
|
1115
1222
|
return this._vectorThresholds.get(indexName);
|
|
1116
1223
|
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Detects PostGIS support by attempting to enable the extension.
|
|
1226
|
+
* Idempotent — safe to call multiple times.
|
|
1227
|
+
*/
|
|
1228
|
+
async _detectGeoSupport() {
|
|
1229
|
+
if (this._supportsGeo !== void 0) return this._supportsGeo;
|
|
1230
|
+
try {
|
|
1231
|
+
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS postgis");
|
|
1232
|
+
this._supportsGeo = true;
|
|
1233
|
+
} catch {
|
|
1234
|
+
this._supportsGeo = false;
|
|
1235
|
+
this.logger.warn("[postgres] PostGIS extension not available — db.geoPoint fields are stored as JSONB (no geo search).");
|
|
1236
|
+
}
|
|
1237
|
+
return this._supportsGeo;
|
|
1238
|
+
}
|
|
1239
|
+
isGeoSearchable() {
|
|
1240
|
+
return this._supportsGeo === true;
|
|
1241
|
+
}
|
|
1242
|
+
async geoSearch(point, query, indexName) {
|
|
1243
|
+
await this._detectGeoSupport();
|
|
1244
|
+
const { sql, params } = this._buildGeoSearchSelect(this._prepareGeoSearch(point, query, indexName));
|
|
1245
|
+
this._log(sql, params);
|
|
1246
|
+
return (await this._exec().all(sql, params)).map((row) => (0, _atscript_db_sql_tools.renameGeoDistance)(row));
|
|
1247
|
+
}
|
|
1248
|
+
async geoSearchWithCount(point, query, indexName) {
|
|
1249
|
+
await this._detectGeoSupport();
|
|
1250
|
+
const ctx = this._prepareGeoSearch(point, query, indexName);
|
|
1251
|
+
const { sql, params } = this._buildGeoSearchSelect(ctx);
|
|
1252
|
+
const countFrag = (0, _atscript_db_sql_tools.buildGeoSearchCount)(pgDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window);
|
|
1253
|
+
this._log(sql, params);
|
|
1254
|
+
this._log(countFrag.sql, countFrag.params);
|
|
1255
|
+
const [rows, countRow] = await Promise.all([this._exec().all(sql, params), this._exec().get(countFrag.sql, countFrag.params)]);
|
|
1256
|
+
return {
|
|
1257
|
+
data: rows.map((row) => (0, _atscript_db_sql_tools.renameGeoDistance)(row)),
|
|
1258
|
+
count: parseCount(countRow?.cnt)
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
1262
|
+
_prepareGeoSearch(point, query, indexName) {
|
|
1263
|
+
if (!this._supportsGeo) throw new _atscript_db.DbError("GEO_NOT_SUPPORTED", [{
|
|
1264
|
+
path: "",
|
|
1265
|
+
message: "Geo search requires the PostGIS extension"
|
|
1266
|
+
}]);
|
|
1267
|
+
const column = this._resolveGeoColumn(indexName);
|
|
1268
|
+
const controls = query.controls ?? {};
|
|
1269
|
+
return {
|
|
1270
|
+
tableName: this.resolveTableName(),
|
|
1271
|
+
where: buildWhere(query.filter),
|
|
1272
|
+
dist: pgGeoDistanceExpr(qi(column), point),
|
|
1273
|
+
window: (0, _atscript_db_sql_tools.geoWindowFromControls)(controls),
|
|
1274
|
+
controls
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
_buildGeoSearchSelect(ctx) {
|
|
1278
|
+
return (0, _atscript_db_sql_tools.buildGeoSearchSelect)(pgDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window, {
|
|
1279
|
+
$limit: ctx.controls.$limit,
|
|
1280
|
+
$skip: ctx.controls.$skip
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1117
1283
|
};
|
|
1118
1284
|
/**
|
|
1119
1285
|
* Normalizes PostgreSQL information_schema data_type values
|
|
@@ -1138,6 +1304,7 @@ function normalizePgType(dataType, maxLength, numericPrecision, numericScale, ud
|
|
|
1138
1304
|
case "user-defined":
|
|
1139
1305
|
if (udtName === "vector") return formattedType;
|
|
1140
1306
|
if (udtName === "citext") return "CITEXT";
|
|
1307
|
+
if (udtName === "geography") return formattedType;
|
|
1141
1308
|
return udtName?.toUpperCase() ?? "USER-DEFINED";
|
|
1142
1309
|
default: return dataType.toUpperCase();
|
|
1143
1310
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -95,6 +95,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
95
95
|
private _nocaseColumns;
|
|
96
96
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
97
97
|
private _citextProvisioned;
|
|
98
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
99
|
+
private _supportsGeo;
|
|
98
100
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
99
101
|
private _supportsVector;
|
|
100
102
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -151,6 +153,9 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
151
153
|
replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
152
154
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
153
155
|
deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
156
|
+
prepareTypeMapper(): Promise<void>;
|
|
157
|
+
/** Whether the table declares any `db.geoPoint` fields (unencrypted). */
|
|
158
|
+
private _hasGeoPointFields;
|
|
154
159
|
ensureTable(): Promise<void>;
|
|
155
160
|
private _ensureView;
|
|
156
161
|
getExistingColumns(): Promise<TExistingColumn[]>;
|
|
@@ -168,6 +173,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
168
173
|
tableExists(): Promise<boolean>;
|
|
169
174
|
dropTable(): Promise<void>;
|
|
170
175
|
dropColumns(columns: string[]): Promise<void>;
|
|
176
|
+
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
171
177
|
dropTableByName(tableName: string): Promise<void>;
|
|
172
178
|
dropViewByName(viewName: string): Promise<void>;
|
|
173
179
|
renameTable(oldName: string): Promise<void>;
|
|
@@ -204,6 +210,20 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
204
210
|
private _buildVectorSearchCountQuery;
|
|
205
211
|
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
206
212
|
private _resolveVectorThreshold;
|
|
213
|
+
/**
|
|
214
|
+
* Detects PostGIS support by attempting to enable the extension.
|
|
215
|
+
* Idempotent — safe to call multiple times.
|
|
216
|
+
*/
|
|
217
|
+
private _detectGeoSupport;
|
|
218
|
+
isGeoSearchable(): boolean;
|
|
219
|
+
geoSearch(point: [number, number], query: DbQuery, indexName?: string): Promise<Array<Record<string, unknown>>>;
|
|
220
|
+
geoSearchWithCount(point: [number, number], query: DbQuery, indexName?: string): Promise<{
|
|
221
|
+
data: Array<Record<string, unknown>>;
|
|
222
|
+
count: number;
|
|
223
|
+
}>;
|
|
224
|
+
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
225
|
+
private _prepareGeoSearch;
|
|
226
|
+
private _buildGeoSearchSelect;
|
|
207
227
|
}
|
|
208
228
|
//#endregion
|
|
209
229
|
//#region src/pg-driver.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -95,6 +95,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
95
95
|
private _nocaseColumns;
|
|
96
96
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
97
97
|
private _citextProvisioned;
|
|
98
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
99
|
+
private _supportsGeo;
|
|
98
100
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
99
101
|
private _supportsVector;
|
|
100
102
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -151,6 +153,9 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
151
153
|
replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
152
154
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
153
155
|
deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
156
|
+
prepareTypeMapper(): Promise<void>;
|
|
157
|
+
/** Whether the table declares any `db.geoPoint` fields (unencrypted). */
|
|
158
|
+
private _hasGeoPointFields;
|
|
154
159
|
ensureTable(): Promise<void>;
|
|
155
160
|
private _ensureView;
|
|
156
161
|
getExistingColumns(): Promise<TExistingColumn[]>;
|
|
@@ -168,6 +173,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
168
173
|
tableExists(): Promise<boolean>;
|
|
169
174
|
dropTable(): Promise<void>;
|
|
170
175
|
dropColumns(columns: string[]): Promise<void>;
|
|
176
|
+
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
171
177
|
dropTableByName(tableName: string): Promise<void>;
|
|
172
178
|
dropViewByName(viewName: string): Promise<void>;
|
|
173
179
|
renameTable(oldName: string): Promise<void>;
|
|
@@ -204,6 +210,20 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
204
210
|
private _buildVectorSearchCountQuery;
|
|
205
211
|
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
206
212
|
private _resolveVectorThreshold;
|
|
213
|
+
/**
|
|
214
|
+
* Detects PostGIS support by attempting to enable the extension.
|
|
215
|
+
* Idempotent — safe to call multiple times.
|
|
216
|
+
*/
|
|
217
|
+
private _detectGeoSupport;
|
|
218
|
+
isGeoSearchable(): boolean;
|
|
219
|
+
geoSearch(point: [number, number], query: DbQuery, indexName?: string): Promise<Array<Record<string, unknown>>>;
|
|
220
|
+
geoSearchWithCount(point: [number, number], query: DbQuery, indexName?: string): Promise<{
|
|
221
|
+
data: Array<Record<string, unknown>>;
|
|
222
|
+
count: number;
|
|
223
|
+
}>;
|
|
224
|
+
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
225
|
+
private _prepareGeoSearch;
|
|
226
|
+
private _buildGeoSearchSelect;
|
|
207
227
|
}
|
|
208
228
|
//#endregion
|
|
209
229
|
//#region src/pg-driver.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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, parseRegexString, refActionToSql } from "@atscript/db-sql-tools";
|
|
2
|
+
import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, refActionToSql, renameGeoDistance } from "@atscript/db-sql-tools";
|
|
3
3
|
//#region src/sql-builder.ts
|
|
4
4
|
/**
|
|
5
5
|
* PostgreSQL-aware default value for a given design type.
|
|
@@ -64,11 +64,50 @@ const pgDialect = {
|
|
|
64
64
|
params: [pattern]
|
|
65
65
|
};
|
|
66
66
|
},
|
|
67
|
+
geoWithin(quotedCol, circle) {
|
|
68
|
+
return {
|
|
69
|
+
sql: `ST_DWithin(${quotedCol}, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)`,
|
|
70
|
+
params: [
|
|
71
|
+
circle.center[0],
|
|
72
|
+
circle.center[1],
|
|
73
|
+
circle.radius
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
},
|
|
67
77
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
68
78
|
paramPlaceholder(index) {
|
|
69
79
|
return `$${index}`;
|
|
70
80
|
}
|
|
71
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Distance-in-meters expression from a geography column to a query point.
|
|
84
|
+
* Used as the `distExpr` of the shared geo search builder.
|
|
85
|
+
*/
|
|
86
|
+
function pgGeoDistanceExpr(quotedCol, point) {
|
|
87
|
+
return {
|
|
88
|
+
sql: `ST_Distance(${quotedCol}, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography)`,
|
|
89
|
+
params: [point[0], point[1]]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Formats a `[lng, lat]` tuple as EWKT — the text input format for geography params. */
|
|
93
|
+
function geoPointToEwkt(point) {
|
|
94
|
+
return `SRID=4326;POINT(${point[0]} ${point[1]})`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parses a hex-encoded (E)WKB point — the raw wire format the pg driver
|
|
98
|
+
* returns for geography columns — back to a `[lng, lat]` tuple.
|
|
99
|
+
* Returns `undefined` for anything that isn't a WKB point.
|
|
100
|
+
*/
|
|
101
|
+
function parseEwkbPointHex(hex) {
|
|
102
|
+
if (hex.length < 42 || !/^[0-9A-Fa-f]+$/.test(hex)) return;
|
|
103
|
+
const buf = Buffer.from(hex, "hex");
|
|
104
|
+
const littleEndian = buf.readUInt8(0) === 1;
|
|
105
|
+
const type = littleEndian ? buf.readUInt32LE(1) : buf.readUInt32BE(1);
|
|
106
|
+
if ((type & 255) !== 1) return;
|
|
107
|
+
const offset = (type & 536870912) !== 0 ? 9 : 5;
|
|
108
|
+
if (buf.length < offset + 16) return;
|
|
109
|
+
return [littleEndian ? buf.readDoubleLE(offset) : buf.readDoubleBE(offset), littleEndian ? buf.readDoubleLE(offset + 8) : buf.readDoubleBE(offset + 8)];
|
|
110
|
+
}
|
|
72
111
|
function buildInsert$1(table, data) {
|
|
73
112
|
return buildInsert(pgDialect, table, data);
|
|
74
113
|
}
|
|
@@ -273,6 +312,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
273
312
|
_nocaseColumns = /* @__PURE__ */ new Set();
|
|
274
313
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
275
314
|
_citextProvisioned = false;
|
|
315
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
316
|
+
_supportsGeo;
|
|
276
317
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
277
318
|
_supportsVector;
|
|
278
319
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -370,11 +411,24 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
370
411
|
* invalid for the pgvector `vector` type — it expects bracket-delimited `[1,2,3]`.
|
|
371
412
|
*/
|
|
372
413
|
formatValue(field) {
|
|
373
|
-
if (
|
|
374
|
-
return {
|
|
414
|
+
if (this._vectorFields.has(field.path)) return {
|
|
375
415
|
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
376
416
|
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
377
417
|
};
|
|
418
|
+
if (field.isGeoPoint && !field.encrypted) return {
|
|
419
|
+
toStorage: (value) => {
|
|
420
|
+
if (!this._supportsGeo) return value;
|
|
421
|
+
const point = normalizeGeoPointValue(value);
|
|
422
|
+
return point ? geoPointToEwkt(point) : value;
|
|
423
|
+
},
|
|
424
|
+
fromStorage: (value) => {
|
|
425
|
+
if (typeof value === "string") {
|
|
426
|
+
const point = parseEwkbPointHex(value);
|
|
427
|
+
if (point) return point;
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
378
432
|
}
|
|
379
433
|
/**
|
|
380
434
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -563,6 +617,14 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
563
617
|
this._log(sql, params);
|
|
564
618
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
565
619
|
}
|
|
620
|
+
async prepareTypeMapper() {
|
|
621
|
+
if (this._supportsVector === void 0 && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
622
|
+
if (this._supportsGeo === void 0 && this._hasGeoPointFields()) await this._detectGeoSupport();
|
|
623
|
+
}
|
|
624
|
+
/** Whether the table declares any `db.geoPoint` fields (unencrypted). */
|
|
625
|
+
_hasGeoPointFields() {
|
|
626
|
+
return this._table.fieldDescriptors.some((fd) => fd.isGeoPoint && !fd.encrypted);
|
|
627
|
+
}
|
|
566
628
|
async ensureTable() {
|
|
567
629
|
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
568
630
|
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
|
|
@@ -571,7 +633,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
571
633
|
const msg = err instanceof Error ? err.message : String(err);
|
|
572
634
|
throw new Error(`Failed to create citext extension for @db.collate 'nocase' columns: ${msg}. Either run 'CREATE EXTENSION citext' as a superuser, or use @db.pg.type "CITEXT" after provisioning the extension manually.`, { cause: err });
|
|
573
635
|
}
|
|
574
|
-
|
|
636
|
+
await this.prepareTypeMapper();
|
|
637
|
+
if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
|
|
575
638
|
if (this._table instanceof AtscriptDbView) return this._ensureView();
|
|
576
639
|
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
577
640
|
incrementFields: this._incrementFields,
|
|
@@ -652,7 +715,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
652
715
|
for (const { field } of diff.typeChanged ?? []) {
|
|
653
716
|
const sqlType = this.typeMapper(field);
|
|
654
717
|
const col = qi(field.physicalName);
|
|
655
|
-
|
|
718
|
+
let ddl;
|
|
719
|
+
if (field.isGeoPoint && sqlType.startsWith("geography")) ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING CASE WHEN ${col} IS NULL THEN NULL ELSE ST_SetSRID(ST_MakePoint((${col}->>0)::float8, (${col}->>1)::float8), 4326)::geography END`;
|
|
720
|
+
else ddl = `ALTER TABLE ${quoteTableName(tableName)} ALTER COLUMN ${col} TYPE ${sqlType} USING ${col}::text::${sqlType}`;
|
|
656
721
|
this._log(ddl);
|
|
657
722
|
await this._exec().exec(ddl);
|
|
658
723
|
}
|
|
@@ -813,6 +878,27 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
813
878
|
this._log(ddl);
|
|
814
879
|
await this._exec().exec(ddl);
|
|
815
880
|
}
|
|
881
|
+
async dropIndexesForColumns(columns) {
|
|
882
|
+
const tableName = this._table.tableName;
|
|
883
|
+
const schema = this._schema;
|
|
884
|
+
const rows = await this._exec().all(`SELECT DISTINCT i.relname AS name
|
|
885
|
+
FROM pg_index x
|
|
886
|
+
JOIN pg_class t ON t.oid = x.indrelid
|
|
887
|
+
JOIN pg_class i ON i.oid = x.indexrelid
|
|
888
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
889
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(x.indkey)
|
|
890
|
+
WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')
|
|
891
|
+
AND i.relname LIKE 'atscript__%' AND a.attname = ANY($3)`, [
|
|
892
|
+
tableName,
|
|
893
|
+
schema,
|
|
894
|
+
columns
|
|
895
|
+
]);
|
|
896
|
+
for (const row of rows) {
|
|
897
|
+
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(row.name)}`;
|
|
898
|
+
this._log(sql);
|
|
899
|
+
await this._exec().exec(sql);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
816
902
|
async dropTableByName(tableName) {
|
|
817
903
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
|
|
818
904
|
this._log(ddl);
|
|
@@ -835,14 +921,29 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
835
921
|
const vec = this._vectorFields.get(field.path);
|
|
836
922
|
return this._supportsVector ? `vector(${vec.dimensions})` : "JSONB";
|
|
837
923
|
}
|
|
924
|
+
if (field.isGeoPoint) return this._supportsGeo ? "geography(Point,4326)" : pgTypeFromField(field);
|
|
838
925
|
return pgTypeFromField(field);
|
|
839
926
|
}
|
|
840
927
|
async syncIndexes() {
|
|
841
928
|
const tableName = this._table.tableName;
|
|
842
929
|
const schema = this._schema;
|
|
930
|
+
await this.prepareTypeMapper();
|
|
843
931
|
await this.syncIndexesWithDiff({
|
|
844
|
-
listExisting: async () =>
|
|
845
|
-
|
|
932
|
+
listExisting: async () => {
|
|
933
|
+
return (await this._exec().all(`SELECT i.relname AS name,
|
|
934
|
+
(SELECT array_agg(a.attname::text ORDER BY k.ord)
|
|
935
|
+
FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
936
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
937
|
+
WHERE k.attnum > 0) AS columns
|
|
938
|
+
FROM pg_index x
|
|
939
|
+
JOIN pg_class t ON t.oid = x.indrelid
|
|
940
|
+
JOIN pg_class i ON i.oid = x.indexrelid
|
|
941
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
942
|
+
WHERE t.relname = $1 AND n.nspname = COALESCE($2, 'public')`, [tableName, schema])).map((r) => ({
|
|
943
|
+
name: r.name,
|
|
944
|
+
columns: r.columns ?? void 0
|
|
945
|
+
}));
|
|
946
|
+
},
|
|
846
947
|
createIndex: async (index) => {
|
|
847
948
|
if (index.type === "fulltext") {
|
|
848
949
|
const tsvectorExpr = this._buildTsvectorExpr(index.fields);
|
|
@@ -851,6 +952,16 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
851
952
|
await this._exec().exec(sql);
|
|
852
953
|
return;
|
|
853
954
|
}
|
|
955
|
+
if (index.type === "geo") {
|
|
956
|
+
if (!this._supportsGeo) {
|
|
957
|
+
this.logger.warn(`[postgres] geo index "${index.name}" declared but PostGIS is not available — skipped`);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const sql = `CREATE INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} USING gist (${qi(index.fields[0].name)})`;
|
|
961
|
+
this._log(sql);
|
|
962
|
+
await this._exec().exec(sql);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
854
965
|
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
855
966
|
const cols = index.fields.map((f) => `${qi(f.name)} ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
856
967
|
const sql = `CREATE ${unique}INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${cols})`;
|
|
@@ -861,10 +972,6 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
861
972
|
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
862
973
|
this._log(sql);
|
|
863
974
|
await this._exec().exec(sql);
|
|
864
|
-
},
|
|
865
|
-
warnUnsupportedTypes: {
|
|
866
|
-
adapter: "postgres",
|
|
867
|
-
types: ["geo"]
|
|
868
975
|
}
|
|
869
976
|
});
|
|
870
977
|
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
@@ -1113,6 +1220,65 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1113
1220
|
if (queryThreshold !== void 0) return queryThreshold;
|
|
1114
1221
|
return this._vectorThresholds.get(indexName);
|
|
1115
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Detects PostGIS support by attempting to enable the extension.
|
|
1225
|
+
* Idempotent — safe to call multiple times.
|
|
1226
|
+
*/
|
|
1227
|
+
async _detectGeoSupport() {
|
|
1228
|
+
if (this._supportsGeo !== void 0) return this._supportsGeo;
|
|
1229
|
+
try {
|
|
1230
|
+
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS postgis");
|
|
1231
|
+
this._supportsGeo = true;
|
|
1232
|
+
} catch {
|
|
1233
|
+
this._supportsGeo = false;
|
|
1234
|
+
this.logger.warn("[postgres] PostGIS extension not available — db.geoPoint fields are stored as JSONB (no geo search).");
|
|
1235
|
+
}
|
|
1236
|
+
return this._supportsGeo;
|
|
1237
|
+
}
|
|
1238
|
+
isGeoSearchable() {
|
|
1239
|
+
return this._supportsGeo === true;
|
|
1240
|
+
}
|
|
1241
|
+
async geoSearch(point, query, indexName) {
|
|
1242
|
+
await this._detectGeoSupport();
|
|
1243
|
+
const { sql, params } = this._buildGeoSearchSelect(this._prepareGeoSearch(point, query, indexName));
|
|
1244
|
+
this._log(sql, params);
|
|
1245
|
+
return (await this._exec().all(sql, params)).map((row) => renameGeoDistance(row));
|
|
1246
|
+
}
|
|
1247
|
+
async geoSearchWithCount(point, query, indexName) {
|
|
1248
|
+
await this._detectGeoSupport();
|
|
1249
|
+
const ctx = this._prepareGeoSearch(point, query, indexName);
|
|
1250
|
+
const { sql, params } = this._buildGeoSearchSelect(ctx);
|
|
1251
|
+
const countFrag = buildGeoSearchCount(pgDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window);
|
|
1252
|
+
this._log(sql, params);
|
|
1253
|
+
this._log(countFrag.sql, countFrag.params);
|
|
1254
|
+
const [rows, countRow] = await Promise.all([this._exec().all(sql, params), this._exec().get(countFrag.sql, countFrag.params)]);
|
|
1255
|
+
return {
|
|
1256
|
+
data: rows.map((row) => renameGeoDistance(row)),
|
|
1257
|
+
count: parseCount(countRow?.cnt)
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
1261
|
+
_prepareGeoSearch(point, query, indexName) {
|
|
1262
|
+
if (!this._supportsGeo) throw new DbError("GEO_NOT_SUPPORTED", [{
|
|
1263
|
+
path: "",
|
|
1264
|
+
message: "Geo search requires the PostGIS extension"
|
|
1265
|
+
}]);
|
|
1266
|
+
const column = this._resolveGeoColumn(indexName);
|
|
1267
|
+
const controls = query.controls ?? {};
|
|
1268
|
+
return {
|
|
1269
|
+
tableName: this.resolveTableName(),
|
|
1270
|
+
where: buildWhere(query.filter),
|
|
1271
|
+
dist: pgGeoDistanceExpr(qi(column), point),
|
|
1272
|
+
window: geoWindowFromControls(controls),
|
|
1273
|
+
controls
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
_buildGeoSearchSelect(ctx) {
|
|
1277
|
+
return buildGeoSearchSelect(pgDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window, {
|
|
1278
|
+
$limit: ctx.controls.$limit,
|
|
1279
|
+
$skip: ctx.controls.$skip
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1116
1282
|
};
|
|
1117
1283
|
/**
|
|
1118
1284
|
* Normalizes PostgreSQL information_schema data_type values
|
|
@@ -1137,6 +1303,7 @@ function normalizePgType(dataType, maxLength, numericPrecision, numericScale, ud
|
|
|
1137
1303
|
case "user-defined":
|
|
1138
1304
|
if (udtName === "vector") return formattedType;
|
|
1139
1305
|
if (udtName === "citext") return "CITEXT";
|
|
1306
|
+
if (udtName === "geography") return formattedType;
|
|
1140
1307
|
return udtName?.toUpperCase() ?? "USER-DEFINED";
|
|
1141
1308
|
default: return dataType.toUpperCase();
|
|
1142
1309
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-postgres",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.106",
|
|
4
4
|
"description": "PostgreSQL adapter for @atscript/db with pg (node-postgres) driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"@atscript/typescript": "^0.1.76",
|
|
56
56
|
"@uniqu/core": "^0.1.6",
|
|
57
57
|
"pg": ">=8.0.0",
|
|
58
|
-
"@atscript/db": "^0.1.
|
|
59
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
58
|
+
"@atscript/db": "^0.1.106",
|
|
59
|
+
"@atscript/db-sql-tools": "^0.1.106"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"pg": {
|