@atscript/db-postgres 0.1.104 → 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 -12
- package/dist/index.d.cts +21 -2
- package/dist/index.d.mts +21 -2
- package/dist/index.mjs +179 -13
- package/dist/plugin.cjs +54 -3
- package/dist/plugin.d.cts +5 -1
- package/dist/plugin.d.mts +5 -1
- package/dist/plugin.mjs +52 -1
- package/package.json +8 -8
- package/dist/index-Cky7JZNO.d.cts +0 -6
- package/dist/index-Cky7JZNO.d.mts +0 -6
- package/dist/plugin-CAuSb5sS.mjs +0 -53
- package/dist/plugin-D8KAYro7.cjs +0 -58
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_plugin = require("./plugin-D8KAYro7.cjs");
|
|
3
2
|
let _atscript_db = require("@atscript/db");
|
|
4
3
|
let _atscript_db_sql_tools = require("@atscript/db-sql-tools");
|
|
5
4
|
//#region src/sql-builder.ts
|
|
@@ -66,11 +65,50 @@ const pgDialect = {
|
|
|
66
65
|
params: [pattern]
|
|
67
66
|
};
|
|
68
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
|
+
},
|
|
69
78
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
70
79
|
paramPlaceholder(index) {
|
|
71
80
|
return `$${index}`;
|
|
72
81
|
}
|
|
73
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
|
+
}
|
|
74
112
|
function buildInsert(table, data) {
|
|
75
113
|
return (0, _atscript_db_sql_tools.buildInsert)(pgDialect, table, data);
|
|
76
114
|
}
|
|
@@ -275,6 +313,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
275
313
|
_nocaseColumns = /* @__PURE__ */ new Set();
|
|
276
314
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
277
315
|
_citextProvisioned = false;
|
|
316
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
317
|
+
_supportsGeo;
|
|
278
318
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
279
319
|
_supportsVector;
|
|
280
320
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -372,11 +412,24 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
372
412
|
* invalid for the pgvector `vector` type — it expects bracket-delimited `[1,2,3]`.
|
|
373
413
|
*/
|
|
374
414
|
formatValue(field) {
|
|
375
|
-
if (
|
|
376
|
-
return {
|
|
415
|
+
if (this._vectorFields.has(field.path)) return {
|
|
377
416
|
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
378
417
|
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
379
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
|
+
};
|
|
380
433
|
}
|
|
381
434
|
/**
|
|
382
435
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -565,6 +618,14 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
565
618
|
this._log(sql, params);
|
|
566
619
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
567
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
|
+
}
|
|
568
629
|
async ensureTable() {
|
|
569
630
|
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
570
631
|
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
|
|
@@ -573,7 +634,8 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
573
634
|
const msg = err instanceof Error ? err.message : String(err);
|
|
574
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 });
|
|
575
636
|
}
|
|
576
|
-
|
|
637
|
+
await this.prepareTypeMapper();
|
|
638
|
+
if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
|
|
577
639
|
if (this._table instanceof _atscript_db.AtscriptDbView) return this._ensureView();
|
|
578
640
|
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
579
641
|
incrementFields: this._incrementFields,
|
|
@@ -654,7 +716,9 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
654
716
|
for (const { field } of diff.typeChanged ?? []) {
|
|
655
717
|
const sqlType = this.typeMapper(field);
|
|
656
718
|
const col = qi(field.physicalName);
|
|
657
|
-
|
|
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}`;
|
|
658
722
|
this._log(ddl);
|
|
659
723
|
await this._exec().exec(ddl);
|
|
660
724
|
}
|
|
@@ -815,6 +879,27 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
815
879
|
this._log(ddl);
|
|
816
880
|
await this._exec().exec(ddl);
|
|
817
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
|
+
}
|
|
818
903
|
async dropTableByName(tableName) {
|
|
819
904
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
|
|
820
905
|
this._log(ddl);
|
|
@@ -837,14 +922,29 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
837
922
|
const vec = this._vectorFields.get(field.path);
|
|
838
923
|
return this._supportsVector ? `vector(${vec.dimensions})` : "JSONB";
|
|
839
924
|
}
|
|
925
|
+
if (field.isGeoPoint) return this._supportsGeo ? "geography(Point,4326)" : pgTypeFromField(field);
|
|
840
926
|
return pgTypeFromField(field);
|
|
841
927
|
}
|
|
842
928
|
async syncIndexes() {
|
|
843
929
|
const tableName = this._table.tableName;
|
|
844
930
|
const schema = this._schema;
|
|
931
|
+
await this.prepareTypeMapper();
|
|
845
932
|
await this.syncIndexesWithDiff({
|
|
846
|
-
listExisting: async () =>
|
|
847
|
-
|
|
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
|
+
},
|
|
848
948
|
createIndex: async (index) => {
|
|
849
949
|
if (index.type === "fulltext") {
|
|
850
950
|
const tsvectorExpr = this._buildTsvectorExpr(index.fields);
|
|
@@ -853,6 +953,16 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
853
953
|
await this._exec().exec(sql);
|
|
854
954
|
return;
|
|
855
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
|
+
}
|
|
856
966
|
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
857
967
|
const cols = index.fields.map((f) => `${qi(f.name)} ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
858
968
|
const sql = `CREATE ${unique}INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${cols})`;
|
|
@@ -863,10 +973,6 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
863
973
|
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
864
974
|
this._log(sql);
|
|
865
975
|
await this._exec().exec(sql);
|
|
866
|
-
},
|
|
867
|
-
warnUnsupportedTypes: {
|
|
868
|
-
adapter: "postgres",
|
|
869
|
-
types: ["geo"]
|
|
870
976
|
}
|
|
871
977
|
});
|
|
872
978
|
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
@@ -1115,6 +1221,65 @@ var PostgresAdapter = class PostgresAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1115
1221
|
if (queryThreshold !== void 0) return queryThreshold;
|
|
1116
1222
|
return this._vectorThresholds.get(indexName);
|
|
1117
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
|
+
}
|
|
1118
1283
|
};
|
|
1119
1284
|
/**
|
|
1120
1285
|
* Normalizes PostgreSQL information_schema data_type values
|
|
@@ -1139,6 +1304,7 @@ function normalizePgType(dataType, maxLength, numericPrecision, numericScale, ud
|
|
|
1139
1304
|
case "user-defined":
|
|
1140
1305
|
if (udtName === "vector") return formattedType;
|
|
1141
1306
|
if (udtName === "citext") return "CITEXT";
|
|
1307
|
+
if (udtName === "geography") return formattedType;
|
|
1142
1308
|
return udtName?.toUpperCase() ?? "USER-DEFINED";
|
|
1143
1309
|
default: return dataType.toUpperCase();
|
|
1144
1310
|
}
|
|
@@ -1365,6 +1531,5 @@ function createAdapter(uri, options) {
|
|
|
1365
1531
|
//#endregion
|
|
1366
1532
|
exports.PgDriver = PgDriver;
|
|
1367
1533
|
exports.PostgresAdapter = PostgresAdapter;
|
|
1368
|
-
exports.PostgresPlugin = require_plugin.PostgresPlugin;
|
|
1369
1534
|
exports.buildWhere = buildWhere;
|
|
1370
1535
|
exports.createAdapter = createAdapter;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { t as PostgresPlugin } from "./index-Cky7JZNO.cjs";
|
|
2
1
|
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TExistingColumn, TFieldOps, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
|
|
3
2
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
4
3
|
import { TSqlFragment } from "@atscript/db-sql-tools";
|
|
@@ -96,6 +95,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
96
95
|
private _nocaseColumns;
|
|
97
96
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
98
97
|
private _citextProvisioned;
|
|
98
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
99
|
+
private _supportsGeo;
|
|
99
100
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
100
101
|
private _supportsVector;
|
|
101
102
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -152,6 +153,9 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
152
153
|
replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
153
154
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
154
155
|
deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
156
|
+
prepareTypeMapper(): Promise<void>;
|
|
157
|
+
/** Whether the table declares any `db.geoPoint` fields (unencrypted). */
|
|
158
|
+
private _hasGeoPointFields;
|
|
155
159
|
ensureTable(): Promise<void>;
|
|
156
160
|
private _ensureView;
|
|
157
161
|
getExistingColumns(): Promise<TExistingColumn[]>;
|
|
@@ -169,6 +173,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
169
173
|
tableExists(): Promise<boolean>;
|
|
170
174
|
dropTable(): Promise<void>;
|
|
171
175
|
dropColumns(columns: string[]): Promise<void>;
|
|
176
|
+
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
172
177
|
dropTableByName(tableName: string): Promise<void>;
|
|
173
178
|
dropViewByName(viewName: string): Promise<void>;
|
|
174
179
|
renameTable(oldName: string): Promise<void>;
|
|
@@ -205,6 +210,20 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
205
210
|
private _buildVectorSearchCountQuery;
|
|
206
211
|
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
207
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;
|
|
208
227
|
}
|
|
209
228
|
//#endregion
|
|
210
229
|
//#region src/pg-driver.d.ts
|
|
@@ -275,4 +294,4 @@ declare function buildWhere(filter: FilterExpr): TSqlFragment;
|
|
|
275
294
|
*/
|
|
276
295
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
277
296
|
//#endregion
|
|
278
|
-
export { PgDriver, PostgresAdapter,
|
|
297
|
+
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { t as PostgresPlugin } from "./index-Cky7JZNO.mjs";
|
|
2
1
|
import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TExistingColumn, TFieldOps, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
|
|
3
2
|
import { TSqlFragment } from "@atscript/db-sql-tools";
|
|
4
3
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
@@ -96,6 +95,8 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
96
95
|
private _nocaseColumns;
|
|
97
96
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
98
97
|
private _citextProvisioned;
|
|
98
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
99
|
+
private _supportsGeo;
|
|
99
100
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
100
101
|
private _supportsVector;
|
|
101
102
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -152,6 +153,9 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
152
153
|
replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
|
|
153
154
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
154
155
|
deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
156
|
+
prepareTypeMapper(): Promise<void>;
|
|
157
|
+
/** Whether the table declares any `db.geoPoint` fields (unencrypted). */
|
|
158
|
+
private _hasGeoPointFields;
|
|
155
159
|
ensureTable(): Promise<void>;
|
|
156
160
|
private _ensureView;
|
|
157
161
|
getExistingColumns(): Promise<TExistingColumn[]>;
|
|
@@ -169,6 +173,7 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
169
173
|
tableExists(): Promise<boolean>;
|
|
170
174
|
dropTable(): Promise<void>;
|
|
171
175
|
dropColumns(columns: string[]): Promise<void>;
|
|
176
|
+
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
172
177
|
dropTableByName(tableName: string): Promise<void>;
|
|
173
178
|
dropViewByName(viewName: string): Promise<void>;
|
|
174
179
|
renameTable(oldName: string): Promise<void>;
|
|
@@ -205,6 +210,20 @@ declare class PostgresAdapter extends BaseDbAdapter {
|
|
|
205
210
|
private _buildVectorSearchCountQuery;
|
|
206
211
|
/** Resolves threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
207
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;
|
|
208
227
|
}
|
|
209
228
|
//#endregion
|
|
210
229
|
//#region src/pg-driver.d.ts
|
|
@@ -275,4 +294,4 @@ declare function buildWhere(filter: FilterExpr): TSqlFragment;
|
|
|
275
294
|
*/
|
|
276
295
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
277
296
|
//#endregion
|
|
278
|
-
export { PgDriver, PostgresAdapter,
|
|
297
|
+
export { PgDriver, PostgresAdapter, type TPgConnection, type TPgDriver, type TPgRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { t as PostgresPlugin } from "./plugin-CAuSb5sS.mjs";
|
|
2
1
|
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
|
|
3
|
-
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";
|
|
4
3
|
//#region src/sql-builder.ts
|
|
5
4
|
/**
|
|
6
5
|
* PostgreSQL-aware default value for a given design type.
|
|
@@ -65,11 +64,50 @@ const pgDialect = {
|
|
|
65
64
|
params: [pattern]
|
|
66
65
|
};
|
|
67
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
|
+
},
|
|
68
77
|
createViewPrefix: "CREATE OR REPLACE VIEW",
|
|
69
78
|
paramPlaceholder(index) {
|
|
70
79
|
return `$${index}`;
|
|
71
80
|
}
|
|
72
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
|
+
}
|
|
73
111
|
function buildInsert$1(table, data) {
|
|
74
112
|
return buildInsert(pgDialect, table, data);
|
|
75
113
|
}
|
|
@@ -274,6 +312,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
274
312
|
_nocaseColumns = /* @__PURE__ */ new Set();
|
|
275
313
|
/** Whether citext extension has been provisioned (avoids redundant round-trips). */
|
|
276
314
|
_citextProvisioned = false;
|
|
315
|
+
/** Whether the connected PostgreSQL instance has the PostGIS extension. */
|
|
316
|
+
_supportsGeo;
|
|
277
317
|
/** Whether the connected PostgreSQL instance has the pgvector extension. */
|
|
278
318
|
_supportsVector;
|
|
279
319
|
/** Vector fields: physical field name → { dimensions, similarity, indexName }. */
|
|
@@ -371,11 +411,24 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
371
411
|
* invalid for the pgvector `vector` type — it expects bracket-delimited `[1,2,3]`.
|
|
372
412
|
*/
|
|
373
413
|
formatValue(field) {
|
|
374
|
-
if (
|
|
375
|
-
return {
|
|
414
|
+
if (this._vectorFields.has(field.path)) return {
|
|
376
415
|
toStorage: (value) => Array.isArray(value) ? `[${value.join(",")}]` : value,
|
|
377
416
|
fromStorage: (value) => typeof value === "string" ? JSON.parse(value) : value
|
|
378
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
|
+
};
|
|
379
432
|
}
|
|
380
433
|
/**
|
|
381
434
|
* Wraps an async write operation to catch PostgreSQL constraint errors
|
|
@@ -564,6 +617,14 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
564
617
|
this._log(sql, params);
|
|
565
618
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
566
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
|
+
}
|
|
567
628
|
async ensureTable() {
|
|
568
629
|
if (this._nocaseColumns.size > 0 && !this._citextProvisioned) try {
|
|
569
630
|
await this._exec().exec("CREATE EXTENSION IF NOT EXISTS citext");
|
|
@@ -572,7 +633,8 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
572
633
|
const msg = err instanceof Error ? err.message : String(err);
|
|
573
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 });
|
|
574
635
|
}
|
|
575
|
-
|
|
636
|
+
await this.prepareTypeMapper();
|
|
637
|
+
if (this._schema) await this._exec().exec(`CREATE SCHEMA IF NOT EXISTS ${qi(this._schema)}`);
|
|
576
638
|
if (this._table instanceof AtscriptDbView) return this._ensureView();
|
|
577
639
|
const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys, {
|
|
578
640
|
incrementFields: this._incrementFields,
|
|
@@ -653,7 +715,9 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
653
715
|
for (const { field } of diff.typeChanged ?? []) {
|
|
654
716
|
const sqlType = this.typeMapper(field);
|
|
655
717
|
const col = qi(field.physicalName);
|
|
656
|
-
|
|
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}`;
|
|
657
721
|
this._log(ddl);
|
|
658
722
|
await this._exec().exec(ddl);
|
|
659
723
|
}
|
|
@@ -814,6 +878,27 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
814
878
|
this._log(ddl);
|
|
815
879
|
await this._exec().exec(ddl);
|
|
816
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
|
+
}
|
|
817
902
|
async dropTableByName(tableName) {
|
|
818
903
|
const ddl = `DROP TABLE IF EXISTS ${quoteTableName(tableName)} CASCADE`;
|
|
819
904
|
this._log(ddl);
|
|
@@ -836,14 +921,29 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
836
921
|
const vec = this._vectorFields.get(field.path);
|
|
837
922
|
return this._supportsVector ? `vector(${vec.dimensions})` : "JSONB";
|
|
838
923
|
}
|
|
924
|
+
if (field.isGeoPoint) return this._supportsGeo ? "geography(Point,4326)" : pgTypeFromField(field);
|
|
839
925
|
return pgTypeFromField(field);
|
|
840
926
|
}
|
|
841
927
|
async syncIndexes() {
|
|
842
928
|
const tableName = this._table.tableName;
|
|
843
929
|
const schema = this._schema;
|
|
930
|
+
await this.prepareTypeMapper();
|
|
844
931
|
await this.syncIndexesWithDiff({
|
|
845
|
-
listExisting: async () =>
|
|
846
|
-
|
|
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
|
+
},
|
|
847
947
|
createIndex: async (index) => {
|
|
848
948
|
if (index.type === "fulltext") {
|
|
849
949
|
const tsvectorExpr = this._buildTsvectorExpr(index.fields);
|
|
@@ -852,6 +952,16 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
852
952
|
await this._exec().exec(sql);
|
|
853
953
|
return;
|
|
854
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
|
+
}
|
|
855
965
|
const unique = index.type === "unique" ? "UNIQUE " : "";
|
|
856
966
|
const cols = index.fields.map((f) => `${qi(f.name)} ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
|
|
857
967
|
const sql = `CREATE ${unique}INDEX IF NOT EXISTS ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${cols})`;
|
|
@@ -862,10 +972,6 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
862
972
|
const sql = `DROP INDEX IF EXISTS ${schema ? `${qi(schema)}.` : ""}${qi(name)}`;
|
|
863
973
|
this._log(sql);
|
|
864
974
|
await this._exec().exec(sql);
|
|
865
|
-
},
|
|
866
|
-
warnUnsupportedTypes: {
|
|
867
|
-
adapter: "postgres",
|
|
868
|
-
types: ["geo"]
|
|
869
975
|
}
|
|
870
976
|
});
|
|
871
977
|
if (this._supportsVector) for (const [field, vec] of this._vectorFields) {
|
|
@@ -1114,6 +1220,65 @@ var PostgresAdapter = class PostgresAdapter extends BaseDbAdapter {
|
|
|
1114
1220
|
if (queryThreshold !== void 0) return queryThreshold;
|
|
1115
1221
|
return this._vectorThresholds.get(indexName);
|
|
1116
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
|
+
}
|
|
1117
1282
|
};
|
|
1118
1283
|
/**
|
|
1119
1284
|
* Normalizes PostgreSQL information_schema data_type values
|
|
@@ -1138,6 +1303,7 @@ function normalizePgType(dataType, maxLength, numericPrecision, numericScale, ud
|
|
|
1138
1303
|
case "user-defined":
|
|
1139
1304
|
if (udtName === "vector") return formattedType;
|
|
1140
1305
|
if (udtName === "citext") return "CITEXT";
|
|
1306
|
+
if (udtName === "geography") return formattedType;
|
|
1141
1307
|
return udtName?.toUpperCase() ?? "USER-DEFINED";
|
|
1142
1308
|
default: return dataType.toUpperCase();
|
|
1143
1309
|
}
|
|
@@ -1362,4 +1528,4 @@ function createAdapter(uri, options) {
|
|
|
1362
1528
|
return new DbSpace(() => new PostgresAdapter(driver));
|
|
1363
1529
|
}
|
|
1364
1530
|
//#endregion
|
|
1365
|
-
export { PgDriver, PostgresAdapter,
|
|
1531
|
+
export { PgDriver, PostgresAdapter, buildWhere, createAdapter };
|
package/dist/plugin.cjs
CHANGED
|
@@ -2,6 +2,57 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
let _atscript_core = require("@atscript/core");
|
|
6
|
+
//#region src/plugin/annotations.ts
|
|
7
|
+
/**
|
|
8
|
+
* PostgreSQL-specific annotations.
|
|
9
|
+
*
|
|
10
|
+
* Merged into the global config under `{ db: { pg: ... } }` so they
|
|
11
|
+
* live alongside core's `@db.table`, `@db.index.*`, etc.
|
|
12
|
+
*
|
|
13
|
+
* These annotations opt-in to PostgreSQL-specific behavior. Files using only
|
|
14
|
+
* portable `@db.*` annotations remain adapter-agnostic.
|
|
15
|
+
*/
|
|
16
|
+
const annotations = {
|
|
17
|
+
type: new _atscript_core.AnnotationSpec({
|
|
18
|
+
description: "Overrides the native PostgreSQL column type.\n\n```atscript\n@db.pg.type \"CITEXT\"\nname: string\n```",
|
|
19
|
+
nodeType: ["prop"],
|
|
20
|
+
multiple: false,
|
|
21
|
+
argument: {
|
|
22
|
+
name: "type",
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Native PostgreSQL column type (e.g., \"CITEXT\", \"INET\", \"MACADDR\")."
|
|
25
|
+
}
|
|
26
|
+
}),
|
|
27
|
+
schema: new _atscript_core.AnnotationSpec({
|
|
28
|
+
description: "Specifies the PostgreSQL schema for the table.\n\n**Default:** `\"public\"`\n\n```atscript\n@db.pg.schema \"analytics\"\nexport interface Events { ... }\n```",
|
|
29
|
+
nodeType: ["interface"],
|
|
30
|
+
multiple: false,
|
|
31
|
+
argument: {
|
|
32
|
+
name: "schema",
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "PostgreSQL schema name (e.g., \"public\", \"analytics\")."
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
collate: new _atscript_core.AnnotationSpec({
|
|
38
|
+
description: "Specifies a native PostgreSQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.pg.collate \"tr-x-icu\"\nname: string\n```",
|
|
39
|
+
nodeType: ["interface", "prop"],
|
|
40
|
+
multiple: false,
|
|
41
|
+
argument: {
|
|
42
|
+
name: "collation",
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Native PostgreSQL collation name (e.g., \"tr-x-icu\", \"C\", \"und-x-icu\")."
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/plugin/index.ts
|
|
50
|
+
const PostgresPlugin = () => ({
|
|
51
|
+
name: "postgres",
|
|
52
|
+
config() {
|
|
53
|
+
return { annotations: { db: { pg: annotations } } };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
exports.PostgresPlugin = PostgresPlugin;
|
|
58
|
+
exports.default = PostgresPlugin;
|
package/dist/plugin.d.cts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TAtscriptPlugin } from "@atscript/core";
|
|
2
|
+
|
|
3
|
+
//#region src/plugin/index.d.ts
|
|
4
|
+
declare const PostgresPlugin: () => TAtscriptPlugin;
|
|
5
|
+
//#endregion
|
|
2
6
|
export { PostgresPlugin, PostgresPlugin as default };
|
package/dist/plugin.d.mts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TAtscriptPlugin } from "@atscript/core";
|
|
2
|
+
|
|
3
|
+
//#region src/plugin/index.d.ts
|
|
4
|
+
declare const PostgresPlugin: () => TAtscriptPlugin;
|
|
5
|
+
//#endregion
|
|
2
6
|
export { PostgresPlugin, PostgresPlugin as default };
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnnotationSpec } from "@atscript/core";
|
|
2
|
+
//#region src/plugin/annotations.ts
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL-specific annotations.
|
|
5
|
+
*
|
|
6
|
+
* Merged into the global config under `{ db: { pg: ... } }` so they
|
|
7
|
+
* live alongside core's `@db.table`, `@db.index.*`, etc.
|
|
8
|
+
*
|
|
9
|
+
* These annotations opt-in to PostgreSQL-specific behavior. Files using only
|
|
10
|
+
* portable `@db.*` annotations remain adapter-agnostic.
|
|
11
|
+
*/
|
|
12
|
+
const annotations = {
|
|
13
|
+
type: new AnnotationSpec({
|
|
14
|
+
description: "Overrides the native PostgreSQL column type.\n\n```atscript\n@db.pg.type \"CITEXT\"\nname: string\n```",
|
|
15
|
+
nodeType: ["prop"],
|
|
16
|
+
multiple: false,
|
|
17
|
+
argument: {
|
|
18
|
+
name: "type",
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Native PostgreSQL column type (e.g., \"CITEXT\", \"INET\", \"MACADDR\")."
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
schema: new AnnotationSpec({
|
|
24
|
+
description: "Specifies the PostgreSQL schema for the table.\n\n**Default:** `\"public\"`\n\n```atscript\n@db.pg.schema \"analytics\"\nexport interface Events { ... }\n```",
|
|
25
|
+
nodeType: ["interface"],
|
|
26
|
+
multiple: false,
|
|
27
|
+
argument: {
|
|
28
|
+
name: "schema",
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "PostgreSQL schema name (e.g., \"public\", \"analytics\")."
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
collate: new AnnotationSpec({
|
|
34
|
+
description: "Specifies a native PostgreSQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.pg.collate \"tr-x-icu\"\nname: string\n```",
|
|
35
|
+
nodeType: ["interface", "prop"],
|
|
36
|
+
multiple: false,
|
|
37
|
+
argument: {
|
|
38
|
+
name: "collation",
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Native PostgreSQL collation name (e.g., \"tr-x-icu\", \"C\", \"und-x-icu\")."
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/plugin/index.ts
|
|
46
|
+
const PostgresPlugin = () => ({
|
|
47
|
+
name: "postgres",
|
|
48
|
+
config() {
|
|
49
|
+
return { annotations: { db: { pg: annotations } } };
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
//#endregion
|
|
2
53
|
export { PostgresPlugin, PostgresPlugin as default };
|
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",
|
|
@@ -43,20 +43,20 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@atscript/core": "^0.1.
|
|
47
|
-
"@atscript/typescript": "^0.1.
|
|
46
|
+
"@atscript/core": "^0.1.76",
|
|
47
|
+
"@atscript/typescript": "^0.1.76",
|
|
48
48
|
"@types/pg": "^8.11.0",
|
|
49
49
|
"@uniqu/core": "^0.1.6",
|
|
50
50
|
"pg": "^8.13.0",
|
|
51
|
-
"unplugin-atscript": "^0.1.
|
|
51
|
+
"unplugin-atscript": "^0.1.76"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/core": "^0.1.
|
|
55
|
-
"@atscript/typescript": "^0.1.
|
|
54
|
+
"@atscript/core": "^0.1.76",
|
|
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": {
|
package/dist/plugin-CAuSb5sS.mjs
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { AnnotationSpec } from "@atscript/core";
|
|
2
|
-
//#region src/plugin/annotations.ts
|
|
3
|
-
/**
|
|
4
|
-
* PostgreSQL-specific annotations.
|
|
5
|
-
*
|
|
6
|
-
* Merged into the global config under `{ db: { pg: ... } }` so they
|
|
7
|
-
* live alongside core's `@db.table`, `@db.index.*`, etc.
|
|
8
|
-
*
|
|
9
|
-
* These annotations opt-in to PostgreSQL-specific behavior. Files using only
|
|
10
|
-
* portable `@db.*` annotations remain adapter-agnostic.
|
|
11
|
-
*/
|
|
12
|
-
const annotations = {
|
|
13
|
-
type: new AnnotationSpec({
|
|
14
|
-
description: "Overrides the native PostgreSQL column type.\n\n```atscript\n@db.pg.type \"CITEXT\"\nname: string\n```",
|
|
15
|
-
nodeType: ["prop"],
|
|
16
|
-
multiple: false,
|
|
17
|
-
argument: {
|
|
18
|
-
name: "type",
|
|
19
|
-
type: "string",
|
|
20
|
-
description: "Native PostgreSQL column type (e.g., \"CITEXT\", \"INET\", \"MACADDR\")."
|
|
21
|
-
}
|
|
22
|
-
}),
|
|
23
|
-
schema: new AnnotationSpec({
|
|
24
|
-
description: "Specifies the PostgreSQL schema for the table.\n\n**Default:** `\"public\"`\n\n```atscript\n@db.pg.schema \"analytics\"\nexport interface Events { ... }\n```",
|
|
25
|
-
nodeType: ["interface"],
|
|
26
|
-
multiple: false,
|
|
27
|
-
argument: {
|
|
28
|
-
name: "schema",
|
|
29
|
-
type: "string",
|
|
30
|
-
description: "PostgreSQL schema name (e.g., \"public\", \"analytics\")."
|
|
31
|
-
}
|
|
32
|
-
}),
|
|
33
|
-
collate: new AnnotationSpec({
|
|
34
|
-
description: "Specifies a native PostgreSQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.pg.collate \"tr-x-icu\"\nname: string\n```",
|
|
35
|
-
nodeType: ["interface", "prop"],
|
|
36
|
-
multiple: false,
|
|
37
|
-
argument: {
|
|
38
|
-
name: "collation",
|
|
39
|
-
type: "string",
|
|
40
|
-
description: "Native PostgreSQL collation name (e.g., \"tr-x-icu\", \"C\", \"und-x-icu\")."
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
};
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/plugin/index.ts
|
|
46
|
-
const PostgresPlugin = () => ({
|
|
47
|
-
name: "postgres",
|
|
48
|
-
config() {
|
|
49
|
-
return { annotations: { db: { pg: annotations } } };
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
//#endregion
|
|
53
|
-
export { PostgresPlugin as t };
|
package/dist/plugin-D8KAYro7.cjs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
let _atscript_core = require("@atscript/core");
|
|
2
|
-
//#region src/plugin/annotations.ts
|
|
3
|
-
/**
|
|
4
|
-
* PostgreSQL-specific annotations.
|
|
5
|
-
*
|
|
6
|
-
* Merged into the global config under `{ db: { pg: ... } }` so they
|
|
7
|
-
* live alongside core's `@db.table`, `@db.index.*`, etc.
|
|
8
|
-
*
|
|
9
|
-
* These annotations opt-in to PostgreSQL-specific behavior. Files using only
|
|
10
|
-
* portable `@db.*` annotations remain adapter-agnostic.
|
|
11
|
-
*/
|
|
12
|
-
const annotations = {
|
|
13
|
-
type: new _atscript_core.AnnotationSpec({
|
|
14
|
-
description: "Overrides the native PostgreSQL column type.\n\n```atscript\n@db.pg.type \"CITEXT\"\nname: string\n```",
|
|
15
|
-
nodeType: ["prop"],
|
|
16
|
-
multiple: false,
|
|
17
|
-
argument: {
|
|
18
|
-
name: "type",
|
|
19
|
-
type: "string",
|
|
20
|
-
description: "Native PostgreSQL column type (e.g., \"CITEXT\", \"INET\", \"MACADDR\")."
|
|
21
|
-
}
|
|
22
|
-
}),
|
|
23
|
-
schema: new _atscript_core.AnnotationSpec({
|
|
24
|
-
description: "Specifies the PostgreSQL schema for the table.\n\n**Default:** `\"public\"`\n\n```atscript\n@db.pg.schema \"analytics\"\nexport interface Events { ... }\n```",
|
|
25
|
-
nodeType: ["interface"],
|
|
26
|
-
multiple: false,
|
|
27
|
-
argument: {
|
|
28
|
-
name: "schema",
|
|
29
|
-
type: "string",
|
|
30
|
-
description: "PostgreSQL schema name (e.g., \"public\", \"analytics\")."
|
|
31
|
-
}
|
|
32
|
-
}),
|
|
33
|
-
collate: new _atscript_core.AnnotationSpec({
|
|
34
|
-
description: "Specifies a native PostgreSQL collation (overrides portable `@db.column.collate`).\n\n```atscript\n@db.pg.collate \"tr-x-icu\"\nname: string\n```",
|
|
35
|
-
nodeType: ["interface", "prop"],
|
|
36
|
-
multiple: false,
|
|
37
|
-
argument: {
|
|
38
|
-
name: "collation",
|
|
39
|
-
type: "string",
|
|
40
|
-
description: "Native PostgreSQL collation name (e.g., \"tr-x-icu\", \"C\", \"und-x-icu\")."
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
};
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/plugin/index.ts
|
|
46
|
-
const PostgresPlugin = () => ({
|
|
47
|
-
name: "postgres",
|
|
48
|
-
config() {
|
|
49
|
-
return { annotations: { db: { pg: annotations } } };
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
//#endregion
|
|
53
|
-
Object.defineProperty(exports, "PostgresPlugin", {
|
|
54
|
-
enumerable: true,
|
|
55
|
-
get: function() {
|
|
56
|
-
return PostgresPlugin;
|
|
57
|
-
}
|
|
58
|
-
});
|