@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 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") throw new _atscript_db.DbError("GEO_NOT_SUPPORTED", [{
35
- path: field,
36
- message: "$geoWithin is not supported by this adapter"
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") throw new DbError("GEO_NOT_SUPPORTED", [{
34
- path: field,
35
- message: "$geoWithin is not supported by this adapter"
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.105",
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.76"
41
+ "unplugin-atscript": "^0.1.77"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@uniqu/core": "^0.1.6",
45
- "@atscript/db": "^0.1.105"
45
+ "@atscript/db": "^0.1.107"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "vp pack",