@atscript/db-sql-tools 0.1.105 → 0.1.107
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 +121 -4
- package/dist/index.d.cts +63 -1
- package/dist/index.d.mts +63 -1
- package/dist/index.mjs +116 -5
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -31,10 +31,13 @@ const EMPTY_OR = {
|
|
|
31
31
|
function createFilterVisitor(dialect) {
|
|
32
32
|
return {
|
|
33
33
|
comparison(field, op, value) {
|
|
34
|
-
if (op === "$geoWithin")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (op === "$geoWithin") {
|
|
35
|
+
if (dialect.geoWithin) return dialect.geoWithin(dialect.quoteIdentifier(field), value);
|
|
36
|
+
throw new _atscript_db.DbError("GEO_NOT_SUPPORTED", [{
|
|
37
|
+
path: field,
|
|
38
|
+
message: "$geoWithin is not supported by this adapter"
|
|
39
|
+
}]);
|
|
40
|
+
}
|
|
38
41
|
const col = dialect.quoteIdentifier(field);
|
|
39
42
|
const v = dialect.toParam(value);
|
|
40
43
|
switch (op) {
|
|
@@ -138,6 +141,113 @@ function buildWhere(dialect, filter) {
|
|
|
138
141
|
return (0, _uniqu_core.walkFilter)(filter, getVisitor(dialect)) ?? EMPTY_AND;
|
|
139
142
|
}
|
|
140
143
|
//#endregion
|
|
144
|
+
//#region src/geo.ts
|
|
145
|
+
/**
|
|
146
|
+
* Internal alias for the computed distance column in geo search queries.
|
|
147
|
+
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
148
|
+
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
149
|
+
* across dialects, so the public name can't be used directly.
|
|
150
|
+
*/
|
|
151
|
+
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
152
|
+
/**
|
|
153
|
+
* Builds a distance-ranked geo search SELECT:
|
|
154
|
+
*
|
|
155
|
+
* ```sql
|
|
156
|
+
* SELECT * FROM (
|
|
157
|
+
* SELECT t.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
158
|
+
* ) _g
|
|
159
|
+
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
160
|
+
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
164
|
+
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
165
|
+
* Placeholders stay `?`-style; callers finalize for `$N` dialects.
|
|
166
|
+
*/
|
|
167
|
+
function buildGeoSearchSelect(dialect, table, where, distExpr, window, controls) {
|
|
168
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
169
|
+
let sql = `SELECT * FROM (${`SELECT ${dialect.quoteTable("t")}.*, ${distExpr.sql} AS ${alias} FROM ${dialect.quoteTable(table)} AS ${dialect.quoteTable("t")} WHERE ${where.sql}`}) AS ${dialect.quoteTable("_g")} WHERE ${alias} IS NOT NULL`;
|
|
170
|
+
const params = [...distExpr.params, ...where.params];
|
|
171
|
+
if (window.maxDistance !== void 0) {
|
|
172
|
+
sql += ` AND ${alias} <= ?`;
|
|
173
|
+
params.push(window.maxDistance);
|
|
174
|
+
}
|
|
175
|
+
if (window.minDistance !== void 0) {
|
|
176
|
+
sql += ` AND ${alias} >= ?`;
|
|
177
|
+
params.push(window.minDistance);
|
|
178
|
+
}
|
|
179
|
+
sql += ` ORDER BY ${alias} ASC`;
|
|
180
|
+
if (controls.$limit !== void 0) {
|
|
181
|
+
sql += ` LIMIT ?`;
|
|
182
|
+
params.push(controls.$limit);
|
|
183
|
+
}
|
|
184
|
+
if (controls.$skip !== void 0) {
|
|
185
|
+
if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
186
|
+
sql += ` OFFSET ?`;
|
|
187
|
+
params.push(controls.$skip);
|
|
188
|
+
}
|
|
189
|
+
return finalizeParams(dialect, {
|
|
190
|
+
sql,
|
|
191
|
+
params
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
196
|
+
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
197
|
+
*/
|
|
198
|
+
function buildGeoSearchCount(dialect, table, where, distExpr, window) {
|
|
199
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
200
|
+
let sql = `SELECT COUNT(*) AS cnt FROM (${`SELECT ${distExpr.sql} AS ${alias} FROM ${dialect.quoteTable(table)} AS ${dialect.quoteTable("t")} WHERE ${where.sql}`}) AS ${dialect.quoteTable("_g")} WHERE ${alias} IS NOT NULL`;
|
|
201
|
+
const params = [...distExpr.params, ...where.params];
|
|
202
|
+
if (window.maxDistance !== void 0) {
|
|
203
|
+
sql += ` AND ${alias} <= ?`;
|
|
204
|
+
params.push(window.maxDistance);
|
|
205
|
+
}
|
|
206
|
+
if (window.minDistance !== void 0) {
|
|
207
|
+
sql += ` AND ${alias} >= ?`;
|
|
208
|
+
params.push(window.minDistance);
|
|
209
|
+
}
|
|
210
|
+
return finalizeParams(dialect, {
|
|
211
|
+
sql,
|
|
212
|
+
params
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
216
|
+
function geoWindowFromControls(controls) {
|
|
217
|
+
return {
|
|
218
|
+
maxDistance: typeof controls?.$maxDistance === "number" ? controls.$maxDistance : void 0,
|
|
219
|
+
minDistance: typeof controls?.$minDistance === "number" ? controls.$minDistance : void 0
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
224
|
+
* the raw tuple or its JSON-string form (the relational field mapper
|
|
225
|
+
* stringifies `storage: json` fields before adapter formatters run).
|
|
226
|
+
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
227
|
+
* which must pass through untouched).
|
|
228
|
+
*/
|
|
229
|
+
function normalizeGeoPointValue(value) {
|
|
230
|
+
let candidate = value;
|
|
231
|
+
if (typeof candidate === "string") {
|
|
232
|
+
if (!candidate.startsWith("[")) return;
|
|
233
|
+
try {
|
|
234
|
+
candidate = JSON.parse(candidate);
|
|
235
|
+
} catch {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(candidate) && candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") return [candidate[0], candidate[1]];
|
|
240
|
+
}
|
|
241
|
+
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
242
|
+
function renameGeoDistance(row) {
|
|
243
|
+
if ("__atscript_distance" in row) {
|
|
244
|
+
const distance = row[GEO_DISTANCE_ALIAS];
|
|
245
|
+
delete row[GEO_DISTANCE_ALIAS];
|
|
246
|
+
row.$distance = typeof distance === "string" ? Number(distance) : distance;
|
|
247
|
+
}
|
|
248
|
+
return row;
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
141
251
|
//#region src/common.ts
|
|
142
252
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
143
253
|
function sqlStringLiteral(value) {
|
|
@@ -147,6 +257,7 @@ function sqlStringLiteral(value) {
|
|
|
147
257
|
function toSqlValue(value) {
|
|
148
258
|
if (value === void 0) return null;
|
|
149
259
|
if (value === null) return null;
|
|
260
|
+
if (value instanceof Uint8Array) return value;
|
|
150
261
|
if (typeof value === "object") return JSON.stringify(value);
|
|
151
262
|
if (typeof value === "boolean") return value ? 1 : 0;
|
|
152
263
|
return value;
|
|
@@ -471,10 +582,13 @@ function parseRegexString(value) {
|
|
|
471
582
|
exports.AGG_FN_SQL = AGG_FN_SQL;
|
|
472
583
|
exports.EMPTY_AND = EMPTY_AND;
|
|
473
584
|
exports.EMPTY_OR = EMPTY_OR;
|
|
585
|
+
exports.GEO_DISTANCE_ALIAS = GEO_DISTANCE_ALIAS;
|
|
474
586
|
exports.buildAggregateCount = buildAggregateCount;
|
|
475
587
|
exports.buildAggregateSelect = buildAggregateSelect;
|
|
476
588
|
exports.buildCreateView = buildCreateView;
|
|
477
589
|
exports.buildDelete = buildDelete;
|
|
590
|
+
exports.buildGeoSearchCount = buildGeoSearchCount;
|
|
591
|
+
exports.buildGeoSearchSelect = buildGeoSearchSelect;
|
|
478
592
|
exports.buildInsert = buildInsert;
|
|
479
593
|
exports.buildProjection = buildProjection;
|
|
480
594
|
exports.buildSelect = buildSelect;
|
|
@@ -484,9 +598,12 @@ exports.createFilterVisitor = createFilterVisitor;
|
|
|
484
598
|
exports.defaultValueForType = defaultValueForType;
|
|
485
599
|
exports.defaultValueToSqlLiteral = defaultValueToSqlLiteral;
|
|
486
600
|
exports.finalizeParams = finalizeParams;
|
|
601
|
+
exports.geoWindowFromControls = geoWindowFromControls;
|
|
602
|
+
exports.normalizeGeoPointValue = normalizeGeoPointValue;
|
|
487
603
|
exports.parseRegexString = parseRegexString;
|
|
488
604
|
exports.queryNodeToSql = queryNodeToSql;
|
|
489
605
|
exports.queryOpToSql = queryOpToSql;
|
|
490
606
|
exports.refActionToSql = refActionToSql;
|
|
607
|
+
exports.renameGeoDistance = renameGeoDistance;
|
|
491
608
|
exports.sqlStringLiteral = sqlStringLiteral;
|
|
492
609
|
exports.toSqlValue = toSqlValue;
|
package/dist/index.d.cts
CHANGED
|
@@ -6,6 +6,11 @@ interface TSqlFragment {
|
|
|
6
6
|
sql: string;
|
|
7
7
|
params: unknown[];
|
|
8
8
|
}
|
|
9
|
+
/** `$geoWithin` circle: `[lng, lat]` center + radius in meters (core-validated). */
|
|
10
|
+
interface TGeoCircle {
|
|
11
|
+
center: [number, number];
|
|
12
|
+
radius: number;
|
|
13
|
+
}
|
|
9
14
|
interface SqlDialect {
|
|
10
15
|
/** Quotes a column/table name */
|
|
11
16
|
quoteIdentifier(name: string): string;
|
|
@@ -19,6 +24,13 @@ interface SqlDialect {
|
|
|
19
24
|
toParam(value: unknown): unknown;
|
|
20
25
|
/** Handle $regex filter */
|
|
21
26
|
regex(quotedCol: string, value: unknown): TSqlFragment;
|
|
27
|
+
/**
|
|
28
|
+
* Handle `$geoWithin` filter — circle search on a `db.geoPoint` column.
|
|
29
|
+
* The circle is pre-validated by the core layer (`center` is a `[lng, lat]`
|
|
30
|
+
* tuple, `radius` a positive number of meters). Dialects without native geo
|
|
31
|
+
* support omit this; the filter visitor then throws `GEO_NOT_SUPPORTED`.
|
|
32
|
+
*/
|
|
33
|
+
geoWithin?(quotedCol: string, circle: TGeoCircle): TSqlFragment;
|
|
22
34
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
23
35
|
createViewPrefix: string;
|
|
24
36
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -42,6 +54,56 @@ declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFra
|
|
|
42
54
|
*/
|
|
43
55
|
declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
|
|
44
56
|
//#endregion
|
|
57
|
+
//#region src/geo.d.ts
|
|
58
|
+
/**
|
|
59
|
+
* Internal alias for the computed distance column in geo search queries.
|
|
60
|
+
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
61
|
+
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
62
|
+
* across dialects, so the public name can't be used directly.
|
|
63
|
+
*/
|
|
64
|
+
declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
65
|
+
/** Distance window for geo search: `$maxDistance` / `$minDistance` in meters. */
|
|
66
|
+
interface TGeoWindow {
|
|
67
|
+
maxDistance?: number;
|
|
68
|
+
minDistance?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Builds a distance-ranked geo search SELECT:
|
|
72
|
+
*
|
|
73
|
+
* ```sql
|
|
74
|
+
* SELECT * FROM (
|
|
75
|
+
* SELECT t.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
76
|
+
* ) _g
|
|
77
|
+
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
78
|
+
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
82
|
+
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
83
|
+
* Placeholders stay `?`-style; callers finalize for `$N` dialects.
|
|
84
|
+
*/
|
|
85
|
+
declare function buildGeoSearchSelect(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow, controls: {
|
|
86
|
+
$limit?: number;
|
|
87
|
+
$skip?: number;
|
|
88
|
+
}): TSqlFragment;
|
|
89
|
+
/**
|
|
90
|
+
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
91
|
+
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
92
|
+
*/
|
|
93
|
+
declare function buildGeoSearchCount(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow): TSqlFragment;
|
|
94
|
+
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
95
|
+
declare function geoWindowFromControls(controls: Record<string, unknown> | undefined): TGeoWindow;
|
|
96
|
+
/**
|
|
97
|
+
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
98
|
+
* the raw tuple or its JSON-string form (the relational field mapper
|
|
99
|
+
* stringifies `storage: json` fields before adapter formatters run).
|
|
100
|
+
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
101
|
+
* which must pass through untouched).
|
|
102
|
+
*/
|
|
103
|
+
declare function normalizeGeoPointValue(value: unknown): [number, number] | undefined;
|
|
104
|
+
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
105
|
+
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
106
|
+
//#endregion
|
|
45
107
|
//#region src/sql-builder.d.ts
|
|
46
108
|
/**
|
|
47
109
|
* Builds an INSERT statement.
|
|
@@ -124,4 +186,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
124
186
|
flags: string;
|
|
125
187
|
};
|
|
126
188
|
//#endregion
|
|
127
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, type SqlDialect, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
|
|
189
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, type SqlDialect, type TGeoCircle, type TGeoWindow, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, sqlStringLiteral, toSqlValue };
|
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,11 @@ interface TSqlFragment {
|
|
|
6
6
|
sql: string;
|
|
7
7
|
params: unknown[];
|
|
8
8
|
}
|
|
9
|
+
/** `$geoWithin` circle: `[lng, lat]` center + radius in meters (core-validated). */
|
|
10
|
+
interface TGeoCircle {
|
|
11
|
+
center: [number, number];
|
|
12
|
+
radius: number;
|
|
13
|
+
}
|
|
9
14
|
interface SqlDialect {
|
|
10
15
|
/** Quotes a column/table name */
|
|
11
16
|
quoteIdentifier(name: string): string;
|
|
@@ -19,6 +24,13 @@ interface SqlDialect {
|
|
|
19
24
|
toParam(value: unknown): unknown;
|
|
20
25
|
/** Handle $regex filter */
|
|
21
26
|
regex(quotedCol: string, value: unknown): TSqlFragment;
|
|
27
|
+
/**
|
|
28
|
+
* Handle `$geoWithin` filter — circle search on a `db.geoPoint` column.
|
|
29
|
+
* The circle is pre-validated by the core layer (`center` is a `[lng, lat]`
|
|
30
|
+
* tuple, `radius` a positive number of meters). Dialects without native geo
|
|
31
|
+
* support omit this; the filter visitor then throws `GEO_NOT_SUPPORTED`.
|
|
32
|
+
*/
|
|
33
|
+
geoWithin?(quotedCol: string, circle: TGeoCircle): TSqlFragment;
|
|
22
34
|
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
23
35
|
createViewPrefix: string;
|
|
24
36
|
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
@@ -42,6 +54,56 @@ declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFra
|
|
|
42
54
|
*/
|
|
43
55
|
declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
|
|
44
56
|
//#endregion
|
|
57
|
+
//#region src/geo.d.ts
|
|
58
|
+
/**
|
|
59
|
+
* Internal alias for the computed distance column in geo search queries.
|
|
60
|
+
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
61
|
+
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
62
|
+
* across dialects, so the public name can't be used directly.
|
|
63
|
+
*/
|
|
64
|
+
declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
65
|
+
/** Distance window for geo search: `$maxDistance` / `$minDistance` in meters. */
|
|
66
|
+
interface TGeoWindow {
|
|
67
|
+
maxDistance?: number;
|
|
68
|
+
minDistance?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Builds a distance-ranked geo search SELECT:
|
|
72
|
+
*
|
|
73
|
+
* ```sql
|
|
74
|
+
* SELECT * FROM (
|
|
75
|
+
* SELECT t.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
76
|
+
* ) _g
|
|
77
|
+
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
78
|
+
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
82
|
+
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
83
|
+
* Placeholders stay `?`-style; callers finalize for `$N` dialects.
|
|
84
|
+
*/
|
|
85
|
+
declare function buildGeoSearchSelect(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow, controls: {
|
|
86
|
+
$limit?: number;
|
|
87
|
+
$skip?: number;
|
|
88
|
+
}): TSqlFragment;
|
|
89
|
+
/**
|
|
90
|
+
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
91
|
+
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
92
|
+
*/
|
|
93
|
+
declare function buildGeoSearchCount(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow): TSqlFragment;
|
|
94
|
+
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
95
|
+
declare function geoWindowFromControls(controls: Record<string, unknown> | undefined): TGeoWindow;
|
|
96
|
+
/**
|
|
97
|
+
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
98
|
+
* the raw tuple or its JSON-string form (the relational field mapper
|
|
99
|
+
* stringifies `storage: json` fields before adapter formatters run).
|
|
100
|
+
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
101
|
+
* which must pass through untouched).
|
|
102
|
+
*/
|
|
103
|
+
declare function normalizeGeoPointValue(value: unknown): [number, number] | undefined;
|
|
104
|
+
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
105
|
+
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
106
|
+
//#endregion
|
|
45
107
|
//#region src/sql-builder.d.ts
|
|
46
108
|
/**
|
|
47
109
|
* Builds an INSERT statement.
|
|
@@ -124,4 +186,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
124
186
|
flags: string;
|
|
125
187
|
};
|
|
126
188
|
//#endregion
|
|
127
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, type SqlDialect, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
|
|
189
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, type SqlDialect, type TGeoCircle, type TGeoWindow, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, sqlStringLiteral, toSqlValue };
|
package/dist/index.mjs
CHANGED
|
@@ -30,10 +30,13 @@ const EMPTY_OR = {
|
|
|
30
30
|
function createFilterVisitor(dialect) {
|
|
31
31
|
return {
|
|
32
32
|
comparison(field, op, value) {
|
|
33
|
-
if (op === "$geoWithin")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
if (op === "$geoWithin") {
|
|
34
|
+
if (dialect.geoWithin) return dialect.geoWithin(dialect.quoteIdentifier(field), value);
|
|
35
|
+
throw new DbError("GEO_NOT_SUPPORTED", [{
|
|
36
|
+
path: field,
|
|
37
|
+
message: "$geoWithin is not supported by this adapter"
|
|
38
|
+
}]);
|
|
39
|
+
}
|
|
37
40
|
const col = dialect.quoteIdentifier(field);
|
|
38
41
|
const v = dialect.toParam(value);
|
|
39
42
|
switch (op) {
|
|
@@ -137,6 +140,113 @@ function buildWhere(dialect, filter) {
|
|
|
137
140
|
return walkFilter(filter, getVisitor(dialect)) ?? EMPTY_AND;
|
|
138
141
|
}
|
|
139
142
|
//#endregion
|
|
143
|
+
//#region src/geo.ts
|
|
144
|
+
/**
|
|
145
|
+
* Internal alias for the computed distance column in geo search queries.
|
|
146
|
+
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
147
|
+
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
148
|
+
* across dialects, so the public name can't be used directly.
|
|
149
|
+
*/
|
|
150
|
+
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
151
|
+
/**
|
|
152
|
+
* Builds a distance-ranked geo search SELECT:
|
|
153
|
+
*
|
|
154
|
+
* ```sql
|
|
155
|
+
* SELECT * FROM (
|
|
156
|
+
* SELECT t.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
157
|
+
* ) _g
|
|
158
|
+
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
159
|
+
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
163
|
+
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
164
|
+
* Placeholders stay `?`-style; callers finalize for `$N` dialects.
|
|
165
|
+
*/
|
|
166
|
+
function buildGeoSearchSelect(dialect, table, where, distExpr, window, controls) {
|
|
167
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
168
|
+
let sql = `SELECT * FROM (${`SELECT ${dialect.quoteTable("t")}.*, ${distExpr.sql} AS ${alias} FROM ${dialect.quoteTable(table)} AS ${dialect.quoteTable("t")} WHERE ${where.sql}`}) AS ${dialect.quoteTable("_g")} WHERE ${alias} IS NOT NULL`;
|
|
169
|
+
const params = [...distExpr.params, ...where.params];
|
|
170
|
+
if (window.maxDistance !== void 0) {
|
|
171
|
+
sql += ` AND ${alias} <= ?`;
|
|
172
|
+
params.push(window.maxDistance);
|
|
173
|
+
}
|
|
174
|
+
if (window.minDistance !== void 0) {
|
|
175
|
+
sql += ` AND ${alias} >= ?`;
|
|
176
|
+
params.push(window.minDistance);
|
|
177
|
+
}
|
|
178
|
+
sql += ` ORDER BY ${alias} ASC`;
|
|
179
|
+
if (controls.$limit !== void 0) {
|
|
180
|
+
sql += ` LIMIT ?`;
|
|
181
|
+
params.push(controls.$limit);
|
|
182
|
+
}
|
|
183
|
+
if (controls.$skip !== void 0) {
|
|
184
|
+
if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
185
|
+
sql += ` OFFSET ?`;
|
|
186
|
+
params.push(controls.$skip);
|
|
187
|
+
}
|
|
188
|
+
return finalizeParams(dialect, {
|
|
189
|
+
sql,
|
|
190
|
+
params
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
195
|
+
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
196
|
+
*/
|
|
197
|
+
function buildGeoSearchCount(dialect, table, where, distExpr, window) {
|
|
198
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
199
|
+
let sql = `SELECT COUNT(*) AS cnt FROM (${`SELECT ${distExpr.sql} AS ${alias} FROM ${dialect.quoteTable(table)} AS ${dialect.quoteTable("t")} WHERE ${where.sql}`}) AS ${dialect.quoteTable("_g")} WHERE ${alias} IS NOT NULL`;
|
|
200
|
+
const params = [...distExpr.params, ...where.params];
|
|
201
|
+
if (window.maxDistance !== void 0) {
|
|
202
|
+
sql += ` AND ${alias} <= ?`;
|
|
203
|
+
params.push(window.maxDistance);
|
|
204
|
+
}
|
|
205
|
+
if (window.minDistance !== void 0) {
|
|
206
|
+
sql += ` AND ${alias} >= ?`;
|
|
207
|
+
params.push(window.minDistance);
|
|
208
|
+
}
|
|
209
|
+
return finalizeParams(dialect, {
|
|
210
|
+
sql,
|
|
211
|
+
params
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
215
|
+
function geoWindowFromControls(controls) {
|
|
216
|
+
return {
|
|
217
|
+
maxDistance: typeof controls?.$maxDistance === "number" ? controls.$maxDistance : void 0,
|
|
218
|
+
minDistance: typeof controls?.$minDistance === "number" ? controls.$minDistance : void 0
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
223
|
+
* the raw tuple or its JSON-string form (the relational field mapper
|
|
224
|
+
* stringifies `storage: json` fields before adapter formatters run).
|
|
225
|
+
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
226
|
+
* which must pass through untouched).
|
|
227
|
+
*/
|
|
228
|
+
function normalizeGeoPointValue(value) {
|
|
229
|
+
let candidate = value;
|
|
230
|
+
if (typeof candidate === "string") {
|
|
231
|
+
if (!candidate.startsWith("[")) return;
|
|
232
|
+
try {
|
|
233
|
+
candidate = JSON.parse(candidate);
|
|
234
|
+
} catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(candidate) && candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") return [candidate[0], candidate[1]];
|
|
239
|
+
}
|
|
240
|
+
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
241
|
+
function renameGeoDistance(row) {
|
|
242
|
+
if ("__atscript_distance" in row) {
|
|
243
|
+
const distance = row[GEO_DISTANCE_ALIAS];
|
|
244
|
+
delete row[GEO_DISTANCE_ALIAS];
|
|
245
|
+
row.$distance = typeof distance === "string" ? Number(distance) : distance;
|
|
246
|
+
}
|
|
247
|
+
return row;
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
140
250
|
//#region src/common.ts
|
|
141
251
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
142
252
|
function sqlStringLiteral(value) {
|
|
@@ -146,6 +256,7 @@ function sqlStringLiteral(value) {
|
|
|
146
256
|
function toSqlValue(value) {
|
|
147
257
|
if (value === void 0) return null;
|
|
148
258
|
if (value === null) return null;
|
|
259
|
+
if (value instanceof Uint8Array) return value;
|
|
149
260
|
if (typeof value === "object") return JSON.stringify(value);
|
|
150
261
|
if (typeof value === "boolean") return value ? 1 : 0;
|
|
151
262
|
return value;
|
|
@@ -467,4 +578,4 @@ function parseRegexString(value) {
|
|
|
467
578
|
};
|
|
468
579
|
}
|
|
469
580
|
//#endregion
|
|
470
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
|
|
581
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, sqlStringLiteral, toSqlValue };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-sql-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.107",
|
|
4
4
|
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@uniqu/core": "^0.1.6",
|
|
41
|
-
"unplugin-atscript": "^0.1.
|
|
41
|
+
"unplugin-atscript": "^0.1.77"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@uniqu/core": "^0.1.6",
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
45
|
+
"@atscript/db": "^0.1.107"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|