@atscript/db-sql-tools 0.1.141 → 0.1.143
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 +215 -111
- package/dist/index.d.cts +70 -8
- package/dist/index.d.mts +70 -8
- package/dist/index.mjs +212 -112
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -173,113 +173,6 @@ function buildWhere(dialect, filter) {
|
|
|
173
173
|
return (0, _uniqu_core.walkFilter)(filter, getVisitor(dialect)) ?? EMPTY_AND;
|
|
174
174
|
}
|
|
175
175
|
//#endregion
|
|
176
|
-
//#region src/geo.ts
|
|
177
|
-
/**
|
|
178
|
-
* Internal alias for the computed distance column in geo search queries.
|
|
179
|
-
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
180
|
-
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
181
|
-
* across dialects, so the public name can't be used directly.
|
|
182
|
-
*/
|
|
183
|
-
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
184
|
-
/**
|
|
185
|
-
* Builds a distance-ranked geo search SELECT:
|
|
186
|
-
*
|
|
187
|
-
* ```sql
|
|
188
|
-
* SELECT * FROM (
|
|
189
|
-
* SELECT t.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
190
|
-
* ) _g
|
|
191
|
-
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
192
|
-
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
193
|
-
* ```
|
|
194
|
-
*
|
|
195
|
-
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
196
|
-
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
197
|
-
* Placeholders stay `?`-style; callers finalize for `$N` dialects.
|
|
198
|
-
*/
|
|
199
|
-
function buildGeoSearchSelect(dialect, table, where, distExpr, window, controls) {
|
|
200
|
-
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
201
|
-
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`;
|
|
202
|
-
const params = [...distExpr.params, ...where.params];
|
|
203
|
-
if (window.maxDistance !== void 0) {
|
|
204
|
-
sql += ` AND ${alias} <= ?`;
|
|
205
|
-
params.push(window.maxDistance);
|
|
206
|
-
}
|
|
207
|
-
if (window.minDistance !== void 0) {
|
|
208
|
-
sql += ` AND ${alias} >= ?`;
|
|
209
|
-
params.push(window.minDistance);
|
|
210
|
-
}
|
|
211
|
-
sql += ` ORDER BY ${alias} ASC`;
|
|
212
|
-
if (controls.$limit !== void 0) {
|
|
213
|
-
sql += ` LIMIT ?`;
|
|
214
|
-
params.push(controls.$limit);
|
|
215
|
-
}
|
|
216
|
-
if (controls.$skip !== void 0) {
|
|
217
|
-
if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
218
|
-
sql += ` OFFSET ?`;
|
|
219
|
-
params.push(controls.$skip);
|
|
220
|
-
}
|
|
221
|
-
return finalizeParams(dialect, {
|
|
222
|
-
sql,
|
|
223
|
-
params
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
228
|
-
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
229
|
-
*/
|
|
230
|
-
function buildGeoSearchCount(dialect, table, where, distExpr, window) {
|
|
231
|
-
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
232
|
-
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`;
|
|
233
|
-
const params = [...distExpr.params, ...where.params];
|
|
234
|
-
if (window.maxDistance !== void 0) {
|
|
235
|
-
sql += ` AND ${alias} <= ?`;
|
|
236
|
-
params.push(window.maxDistance);
|
|
237
|
-
}
|
|
238
|
-
if (window.minDistance !== void 0) {
|
|
239
|
-
sql += ` AND ${alias} >= ?`;
|
|
240
|
-
params.push(window.minDistance);
|
|
241
|
-
}
|
|
242
|
-
return finalizeParams(dialect, {
|
|
243
|
-
sql,
|
|
244
|
-
params
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
248
|
-
function geoWindowFromControls(controls) {
|
|
249
|
-
return {
|
|
250
|
-
maxDistance: typeof controls?.$maxDistance === "number" ? controls.$maxDistance : void 0,
|
|
251
|
-
minDistance: typeof controls?.$minDistance === "number" ? controls.$minDistance : void 0
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
256
|
-
* the raw tuple or its JSON-string form (the relational field mapper
|
|
257
|
-
* stringifies `storage: json` fields before adapter formatters run).
|
|
258
|
-
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
259
|
-
* which must pass through untouched).
|
|
260
|
-
*/
|
|
261
|
-
function normalizeGeoPointValue(value) {
|
|
262
|
-
let candidate = value;
|
|
263
|
-
if (typeof candidate === "string") {
|
|
264
|
-
if (!candidate.startsWith("[")) return;
|
|
265
|
-
try {
|
|
266
|
-
candidate = JSON.parse(candidate);
|
|
267
|
-
} catch {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (Array.isArray(candidate) && candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") return [candidate[0], candidate[1]];
|
|
272
|
-
}
|
|
273
|
-
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
274
|
-
function renameGeoDistance(row) {
|
|
275
|
-
if ("__atscript_distance" in row) {
|
|
276
|
-
const distance = row[GEO_DISTANCE_ALIAS];
|
|
277
|
-
delete row[GEO_DISTANCE_ALIAS];
|
|
278
|
-
row.$distance = typeof distance === "string" ? Number(distance) : distance;
|
|
279
|
-
}
|
|
280
|
-
return row;
|
|
281
|
-
}
|
|
282
|
-
//#endregion
|
|
283
176
|
//#region src/common.ts
|
|
284
177
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
285
178
|
function sqlStringLiteral(value) {
|
|
@@ -891,16 +784,223 @@ function derivedColumnExpr(dialect, field) {
|
|
|
891
784
|
}
|
|
892
785
|
/**
|
|
893
786
|
* Builds a column projection (SELECT clause fields).
|
|
787
|
+
*
|
|
788
|
+
* @param qualifier - optional table alias every column (and the `*`
|
|
789
|
+
* fallback) is qualified with (`"t"."col"`), for projections over a joined
|
|
790
|
+
* or aliased source such as the geo / vector search subqueries (since 0.1.143).
|
|
894
791
|
*/
|
|
895
|
-
function buildProjection(dialect, select) {
|
|
792
|
+
function buildProjection(dialect, select, qualifier) {
|
|
793
|
+
const prefix = qualifier === void 0 ? "" : `${dialect.quoteTable(qualifier)}.`;
|
|
896
794
|
const fields = select?.asArray;
|
|
897
|
-
if (!fields) return
|
|
795
|
+
if (!fields) return `${prefix}*`;
|
|
898
796
|
let sql = "";
|
|
899
797
|
for (let i = 0; i < fields.length; i++) {
|
|
900
798
|
if (i > 0) sql += ", ";
|
|
901
|
-
sql += dialect.quoteIdentifier(fields[i]);
|
|
799
|
+
sql += prefix + dialect.quoteIdentifier(fields[i]);
|
|
800
|
+
}
|
|
801
|
+
return sql || `${prefix}*`;
|
|
802
|
+
}
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/geo.ts
|
|
805
|
+
/**
|
|
806
|
+
* Internal alias for the computed distance column in geo search queries.
|
|
807
|
+
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
808
|
+
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
809
|
+
* across dialects, so the public name can't be used directly.
|
|
810
|
+
*/
|
|
811
|
+
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
812
|
+
/**
|
|
813
|
+
* Builds a distance-ranked geo search SELECT:
|
|
814
|
+
*
|
|
815
|
+
* ```sql
|
|
816
|
+
* SELECT * FROM (
|
|
817
|
+
* SELECT <t.cols | t.*>, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
818
|
+
* ) _g
|
|
819
|
+
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
820
|
+
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
821
|
+
* ```
|
|
822
|
+
*
|
|
823
|
+
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
824
|
+
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
825
|
+
* `controls.$select` projects the row columns exactly like `buildSelect` does
|
|
826
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`); the
|
|
827
|
+
* distance column is always returned. Placeholders stay `?`-style; callers
|
|
828
|
+
* finalize for `$N` dialects.
|
|
829
|
+
*/
|
|
830
|
+
function buildGeoSearchSelect(dialect, table, where, distExpr, window, controls) {
|
|
831
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
832
|
+
let sql = `SELECT * FROM (${`SELECT ${buildProjection(dialect, controls.$select, "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`;
|
|
833
|
+
const params = [...distExpr.params, ...where.params];
|
|
834
|
+
if (window.maxDistance !== void 0) {
|
|
835
|
+
sql += ` AND ${alias} <= ?`;
|
|
836
|
+
params.push(window.maxDistance);
|
|
837
|
+
}
|
|
838
|
+
if (window.minDistance !== void 0) {
|
|
839
|
+
sql += ` AND ${alias} >= ?`;
|
|
840
|
+
params.push(window.minDistance);
|
|
841
|
+
}
|
|
842
|
+
sql += ` ORDER BY ${alias} ASC`;
|
|
843
|
+
if (controls.$limit !== void 0) {
|
|
844
|
+
sql += ` LIMIT ?`;
|
|
845
|
+
params.push(controls.$limit);
|
|
846
|
+
}
|
|
847
|
+
if (controls.$skip !== void 0) {
|
|
848
|
+
if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
849
|
+
sql += ` OFFSET ?`;
|
|
850
|
+
params.push(controls.$skip);
|
|
851
|
+
}
|
|
852
|
+
return finalizeParams(dialect, {
|
|
853
|
+
sql,
|
|
854
|
+
params
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
859
|
+
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
860
|
+
*/
|
|
861
|
+
function buildGeoSearchCount(dialect, table, where, distExpr, window) {
|
|
862
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
863
|
+
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`;
|
|
864
|
+
const params = [...distExpr.params, ...where.params];
|
|
865
|
+
if (window.maxDistance !== void 0) {
|
|
866
|
+
sql += ` AND ${alias} <= ?`;
|
|
867
|
+
params.push(window.maxDistance);
|
|
868
|
+
}
|
|
869
|
+
if (window.minDistance !== void 0) {
|
|
870
|
+
sql += ` AND ${alias} >= ?`;
|
|
871
|
+
params.push(window.minDistance);
|
|
872
|
+
}
|
|
873
|
+
return finalizeParams(dialect, {
|
|
874
|
+
sql,
|
|
875
|
+
params
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
879
|
+
function geoWindowFromControls(controls) {
|
|
880
|
+
return {
|
|
881
|
+
maxDistance: typeof controls?.$maxDistance === "number" ? controls.$maxDistance : void 0,
|
|
882
|
+
minDistance: typeof controls?.$minDistance === "number" ? controls.$minDistance : void 0
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
887
|
+
* the raw tuple or its JSON-string form (the relational field mapper
|
|
888
|
+
* stringifies `storage: json` fields before adapter formatters run).
|
|
889
|
+
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
890
|
+
* which must pass through untouched).
|
|
891
|
+
*/
|
|
892
|
+
function normalizeGeoPointValue(value) {
|
|
893
|
+
let candidate = value;
|
|
894
|
+
if (typeof candidate === "string") {
|
|
895
|
+
if (!candidate.startsWith("[")) return;
|
|
896
|
+
try {
|
|
897
|
+
candidate = JSON.parse(candidate);
|
|
898
|
+
} catch {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
902
901
|
}
|
|
903
|
-
|
|
902
|
+
if (Array.isArray(candidate) && candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") return [candidate[0], candidate[1]];
|
|
903
|
+
}
|
|
904
|
+
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
905
|
+
function renameGeoDistance(row) {
|
|
906
|
+
if ("__atscript_distance" in row) {
|
|
907
|
+
const distance = row[GEO_DISTANCE_ALIAS];
|
|
908
|
+
delete row[GEO_DISTANCE_ALIAS];
|
|
909
|
+
row.$distance = typeof distance === "string" ? Number(distance) : distance;
|
|
910
|
+
}
|
|
911
|
+
return row;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/vector.ts
|
|
915
|
+
/** Column every vector search row carries: the engine's distance to the query vector. */
|
|
916
|
+
const VECTOR_DISTANCE_ALIAS = "_distance";
|
|
917
|
+
/** Alias of the row source inside the vector search queries. */
|
|
918
|
+
const SOURCE_ALIAS = "_v";
|
|
919
|
+
/**
|
|
920
|
+
* The row source of a vector search over a plain table:
|
|
921
|
+
*
|
|
922
|
+
* ```sql
|
|
923
|
+
* SELECT t.*, <distExpr> AS _distance FROM <table> t WHERE <where>
|
|
924
|
+
* ```
|
|
925
|
+
*
|
|
926
|
+
* `withRows: false` selects the distance only (the count companion).
|
|
927
|
+
* Placeholders stay `?`-style.
|
|
928
|
+
* @since 0.1.143
|
|
929
|
+
*/
|
|
930
|
+
function vectorDistanceSource(dialect, table, where, distExpr, withRows = true) {
|
|
931
|
+
const t = dialect.quoteTable("t");
|
|
932
|
+
return {
|
|
933
|
+
sql: `SELECT ${withRows ? `${t}.*, ` : ""}${distExpr.sql} AS ${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)} FROM ${dialect.quoteTable(table)} AS ${t} WHERE ${where.sql}`,
|
|
934
|
+
params: [...distExpr.params, ...where.params]
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/** The outer WHERE of a vector search: the distance cap, then a residual filter over the source. */
|
|
938
|
+
function outerWhere(dialect, maxDistance, residual) {
|
|
939
|
+
const parts = [];
|
|
940
|
+
const params = [];
|
|
941
|
+
if (maxDistance !== void 0) {
|
|
942
|
+
parts.push(`${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)} <= ?`);
|
|
943
|
+
params.push(maxDistance);
|
|
944
|
+
}
|
|
945
|
+
if (residual && residual.sql !== "1=1") {
|
|
946
|
+
parts.push(`(${residual.sql})`);
|
|
947
|
+
params.push(...residual.params);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
sql: parts.length > 0 ? ` WHERE ${parts.join(" AND ")}` : "",
|
|
951
|
+
params
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Builds a distance-ranked vector search SELECT over a row source that
|
|
956
|
+
* carries a `_distance` column ({@link vectorDistanceSource}, or an
|
|
957
|
+
* engine-specific one such as a vector-index join):
|
|
958
|
+
*
|
|
959
|
+
* ```sql
|
|
960
|
+
* SELECT <_v.cols, _v._distance | *> FROM (<source>) _v
|
|
961
|
+
* [WHERE _distance <= ? [AND (<residual>)]]
|
|
962
|
+
* ORDER BY _distance ASC LIMIT ? [OFFSET ?]
|
|
963
|
+
* ```
|
|
964
|
+
*
|
|
965
|
+
* `$select` projects the OUTER query exactly like `buildSelect` does
|
|
966
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`), so a
|
|
967
|
+
* residual filter still sees every source column; `_distance` is always
|
|
968
|
+
* returned. `maxDistance` is the threshold on the engine's distance scale;
|
|
969
|
+
* a `residual` filter references the source's columns qualified by `_v`.
|
|
970
|
+
* Placeholders are finalized for the dialect.
|
|
971
|
+
* @since 0.1.143
|
|
972
|
+
*/
|
|
973
|
+
function buildVectorSearchSelect(dialect, source, opts) {
|
|
974
|
+
const v = dialect.quoteTable(SOURCE_ALIAS);
|
|
975
|
+
const cols = opts.select?.asArray?.length ? `${buildProjection(dialect, opts.select, SOURCE_ALIAS)}, ${v}.${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)}` : "*";
|
|
976
|
+
const where = outerWhere(dialect, opts.maxDistance, opts.residual);
|
|
977
|
+
let sql = `SELECT ${cols} FROM (${source.sql}) AS ${v}${where.sql} ORDER BY ${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)} ASC LIMIT ?`;
|
|
978
|
+
const params = [
|
|
979
|
+
...source.params,
|
|
980
|
+
...where.params,
|
|
981
|
+
opts.limit
|
|
982
|
+
];
|
|
983
|
+
if (opts.skip) {
|
|
984
|
+
sql += ` OFFSET ?`;
|
|
985
|
+
params.push(opts.skip);
|
|
986
|
+
}
|
|
987
|
+
return finalizeParams(dialect, {
|
|
988
|
+
sql,
|
|
989
|
+
params
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Count companion for {@link buildVectorSearchSelect} — rows the search
|
|
994
|
+
* could return (distance cap and residual applied, pagination ignored).
|
|
995
|
+
* Returns one row: `{ cnt }`.
|
|
996
|
+
* @since 0.1.143
|
|
997
|
+
*/
|
|
998
|
+
function buildVectorSearchCount(dialect, source, opts) {
|
|
999
|
+
const where = outerWhere(dialect, opts.maxDistance, opts.residual);
|
|
1000
|
+
return finalizeParams(dialect, {
|
|
1001
|
+
sql: `SELECT COUNT(*) AS cnt FROM (${source.sql}) AS ${dialect.quoteTable(SOURCE_ALIAS)}${where.sql}`,
|
|
1002
|
+
params: [...source.params, ...where.params]
|
|
1003
|
+
});
|
|
904
1004
|
}
|
|
905
1005
|
//#endregion
|
|
906
1006
|
//#region src/regex.ts
|
|
@@ -933,6 +1033,7 @@ exports.EMPTY_AND = EMPTY_AND;
|
|
|
933
1033
|
exports.EMPTY_OR = EMPTY_OR;
|
|
934
1034
|
exports.GEO_DISTANCE_ALIAS = GEO_DISTANCE_ALIAS;
|
|
935
1035
|
exports.SQL_DEFAULT = SQL_DEFAULT;
|
|
1036
|
+
exports.VECTOR_DISTANCE_ALIAS = VECTOR_DISTANCE_ALIAS;
|
|
936
1037
|
exports.buildAggregateCount = buildAggregateCount;
|
|
937
1038
|
exports.buildAggregateSelect = buildAggregateSelect;
|
|
938
1039
|
exports.buildCreateView = buildCreateView;
|
|
@@ -944,6 +1045,8 @@ exports.buildInsertMany = buildInsertMany;
|
|
|
944
1045
|
exports.buildProjection = buildProjection;
|
|
945
1046
|
exports.buildSelect = buildSelect;
|
|
946
1047
|
exports.buildUpdate = buildUpdate;
|
|
1048
|
+
exports.buildVectorSearchCount = buildVectorSearchCount;
|
|
1049
|
+
exports.buildVectorSearchSelect = buildVectorSearchSelect;
|
|
947
1050
|
exports.buildWhere = buildWhere;
|
|
948
1051
|
exports.createFilterVisitor = createFilterVisitor;
|
|
949
1052
|
exports.defaultValueForType = defaultValueForType;
|
|
@@ -966,3 +1069,4 @@ exports.replaceColumnsFor = replaceColumnsFor;
|
|
|
966
1069
|
exports.sqlStringLiteral = sqlStringLiteral;
|
|
967
1070
|
exports.sqlTimeZoneLiteral = sqlTimeZoneLiteral;
|
|
968
1071
|
exports.toSqlValue = toSqlValue;
|
|
1072
|
+
exports.vectorDistanceSource = vectorDistanceSource;
|
package/dist/index.d.cts
CHANGED
|
@@ -135,6 +135,12 @@ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragme
|
|
|
135
135
|
* across dialects, so the public name can't be used directly.
|
|
136
136
|
*/
|
|
137
137
|
declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
138
|
+
/** The query controls a geo search page reads — an adapter passes its `query.controls` as-is. */
|
|
139
|
+
interface TGeoSearchControls {
|
|
140
|
+
$limit?: number;
|
|
141
|
+
$skip?: number;
|
|
142
|
+
$select?: UniquSelect;
|
|
143
|
+
}
|
|
138
144
|
/** Distance window for geo search: `$maxDistance` / `$minDistance` in meters. */
|
|
139
145
|
interface TGeoWindow {
|
|
140
146
|
maxDistance?: number;
|
|
@@ -145,7 +151,7 @@ interface TGeoWindow {
|
|
|
145
151
|
*
|
|
146
152
|
* ```sql
|
|
147
153
|
* SELECT * FROM (
|
|
148
|
-
* SELECT t
|
|
154
|
+
* SELECT <t.cols | t.*>, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
149
155
|
* ) _g
|
|
150
156
|
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
151
157
|
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
@@ -153,12 +159,12 @@ interface TGeoWindow {
|
|
|
153
159
|
*
|
|
154
160
|
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
155
161
|
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
156
|
-
*
|
|
162
|
+
* `controls.$select` projects the row columns exactly like `buildSelect` does
|
|
163
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`); the
|
|
164
|
+
* distance column is always returned. Placeholders stay `?`-style; callers
|
|
165
|
+
* finalize for `$N` dialects.
|
|
157
166
|
*/
|
|
158
|
-
declare function buildGeoSearchSelect(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow, controls:
|
|
159
|
-
$limit?: number;
|
|
160
|
-
$skip?: number;
|
|
161
|
-
}): TSqlFragment;
|
|
167
|
+
declare function buildGeoSearchSelect(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow, controls: TGeoSearchControls): TSqlFragment;
|
|
162
168
|
/**
|
|
163
169
|
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
164
170
|
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
@@ -177,6 +183,58 @@ declare function normalizeGeoPointValue(value: unknown): [number, number] | unde
|
|
|
177
183
|
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
178
184
|
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
179
185
|
//#endregion
|
|
186
|
+
//#region src/vector.d.ts
|
|
187
|
+
/** Column every vector search row carries: the engine's distance to the query vector. */
|
|
188
|
+
declare const VECTOR_DISTANCE_ALIAS = "_distance";
|
|
189
|
+
/**
|
|
190
|
+
* The row source of a vector search over a plain table:
|
|
191
|
+
*
|
|
192
|
+
* ```sql
|
|
193
|
+
* SELECT t.*, <distExpr> AS _distance FROM <table> t WHERE <where>
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* `withRows: false` selects the distance only (the count companion).
|
|
197
|
+
* Placeholders stay `?`-style.
|
|
198
|
+
* @since 0.1.143
|
|
199
|
+
*/
|
|
200
|
+
declare function vectorDistanceSource(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, withRows?: boolean): TSqlFragment;
|
|
201
|
+
/**
|
|
202
|
+
* Builds a distance-ranked vector search SELECT over a row source that
|
|
203
|
+
* carries a `_distance` column ({@link vectorDistanceSource}, or an
|
|
204
|
+
* engine-specific one such as a vector-index join):
|
|
205
|
+
*
|
|
206
|
+
* ```sql
|
|
207
|
+
* SELECT <_v.cols, _v._distance | *> FROM (<source>) _v
|
|
208
|
+
* [WHERE _distance <= ? [AND (<residual>)]]
|
|
209
|
+
* ORDER BY _distance ASC LIMIT ? [OFFSET ?]
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* `$select` projects the OUTER query exactly like `buildSelect` does
|
|
213
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`), so a
|
|
214
|
+
* residual filter still sees every source column; `_distance` is always
|
|
215
|
+
* returned. `maxDistance` is the threshold on the engine's distance scale;
|
|
216
|
+
* a `residual` filter references the source's columns qualified by `_v`.
|
|
217
|
+
* Placeholders are finalized for the dialect.
|
|
218
|
+
* @since 0.1.143
|
|
219
|
+
*/
|
|
220
|
+
declare function buildVectorSearchSelect(dialect: SqlDialect, source: TSqlFragment, opts: {
|
|
221
|
+
select?: UniquSelect;
|
|
222
|
+
limit: number;
|
|
223
|
+
skip?: number;
|
|
224
|
+
maxDistance?: number;
|
|
225
|
+
residual?: TSqlFragment;
|
|
226
|
+
}): TSqlFragment;
|
|
227
|
+
/**
|
|
228
|
+
* Count companion for {@link buildVectorSearchSelect} — rows the search
|
|
229
|
+
* could return (distance cap and residual applied, pagination ignored).
|
|
230
|
+
* Returns one row: `{ cnt }`.
|
|
231
|
+
* @since 0.1.143
|
|
232
|
+
*/
|
|
233
|
+
declare function buildVectorSearchCount(dialect: SqlDialect, source: TSqlFragment, opts: {
|
|
234
|
+
maxDistance?: number;
|
|
235
|
+
residual?: TSqlFragment;
|
|
236
|
+
}): TSqlFragment;
|
|
237
|
+
//#endregion
|
|
180
238
|
//#region src/view-builder.d.ts
|
|
181
239
|
/**
|
|
182
240
|
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
@@ -286,8 +344,12 @@ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFrag
|
|
|
286
344
|
declare function derivedColumnExpr(dialect: SqlDialect, field: TDbFieldMeta): string;
|
|
287
345
|
/**
|
|
288
346
|
* Builds a column projection (SELECT clause fields).
|
|
347
|
+
*
|
|
348
|
+
* @param qualifier - optional table alias every column (and the `*`
|
|
349
|
+
* fallback) is qualified with (`"t"."col"`), for projections over a joined
|
|
350
|
+
* or aliased source such as the geo / vector search subqueries (since 0.1.143).
|
|
289
351
|
*/
|
|
290
|
-
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
352
|
+
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect, qualifier?: string): string;
|
|
291
353
|
//#endregion
|
|
292
354
|
//#region src/common.d.ts
|
|
293
355
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
@@ -384,4 +446,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
384
446
|
flags: string;
|
|
385
447
|
};
|
|
386
448
|
//#endregion
|
|
387
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
|
449
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
|
package/dist/index.d.mts
CHANGED
|
@@ -135,6 +135,12 @@ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragme
|
|
|
135
135
|
* across dialects, so the public name can't be used directly.
|
|
136
136
|
*/
|
|
137
137
|
declare const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
138
|
+
/** The query controls a geo search page reads — an adapter passes its `query.controls` as-is. */
|
|
139
|
+
interface TGeoSearchControls {
|
|
140
|
+
$limit?: number;
|
|
141
|
+
$skip?: number;
|
|
142
|
+
$select?: UniquSelect;
|
|
143
|
+
}
|
|
138
144
|
/** Distance window for geo search: `$maxDistance` / `$minDistance` in meters. */
|
|
139
145
|
interface TGeoWindow {
|
|
140
146
|
maxDistance?: number;
|
|
@@ -145,7 +151,7 @@ interface TGeoWindow {
|
|
|
145
151
|
*
|
|
146
152
|
* ```sql
|
|
147
153
|
* SELECT * FROM (
|
|
148
|
-
* SELECT t
|
|
154
|
+
* SELECT <t.cols | t.*>, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
149
155
|
* ) _g
|
|
150
156
|
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
151
157
|
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
@@ -153,12 +159,12 @@ interface TGeoWindow {
|
|
|
153
159
|
*
|
|
154
160
|
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
155
161
|
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
156
|
-
*
|
|
162
|
+
* `controls.$select` projects the row columns exactly like `buildSelect` does
|
|
163
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`); the
|
|
164
|
+
* distance column is always returned. Placeholders stay `?`-style; callers
|
|
165
|
+
* finalize for `$N` dialects.
|
|
157
166
|
*/
|
|
158
|
-
declare function buildGeoSearchSelect(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow, controls:
|
|
159
|
-
$limit?: number;
|
|
160
|
-
$skip?: number;
|
|
161
|
-
}): TSqlFragment;
|
|
167
|
+
declare function buildGeoSearchSelect(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, window: TGeoWindow, controls: TGeoSearchControls): TSqlFragment;
|
|
162
168
|
/**
|
|
163
169
|
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
164
170
|
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
@@ -177,6 +183,58 @@ declare function normalizeGeoPointValue(value: unknown): [number, number] | unde
|
|
|
177
183
|
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
178
184
|
declare function renameGeoDistance(row: Record<string, unknown>): Record<string, unknown>;
|
|
179
185
|
//#endregion
|
|
186
|
+
//#region src/vector.d.ts
|
|
187
|
+
/** Column every vector search row carries: the engine's distance to the query vector. */
|
|
188
|
+
declare const VECTOR_DISTANCE_ALIAS = "_distance";
|
|
189
|
+
/**
|
|
190
|
+
* The row source of a vector search over a plain table:
|
|
191
|
+
*
|
|
192
|
+
* ```sql
|
|
193
|
+
* SELECT t.*, <distExpr> AS _distance FROM <table> t WHERE <where>
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* `withRows: false` selects the distance only (the count companion).
|
|
197
|
+
* Placeholders stay `?`-style.
|
|
198
|
+
* @since 0.1.143
|
|
199
|
+
*/
|
|
200
|
+
declare function vectorDistanceSource(dialect: SqlDialect, table: string, where: TSqlFragment, distExpr: TSqlFragment, withRows?: boolean): TSqlFragment;
|
|
201
|
+
/**
|
|
202
|
+
* Builds a distance-ranked vector search SELECT over a row source that
|
|
203
|
+
* carries a `_distance` column ({@link vectorDistanceSource}, or an
|
|
204
|
+
* engine-specific one such as a vector-index join):
|
|
205
|
+
*
|
|
206
|
+
* ```sql
|
|
207
|
+
* SELECT <_v.cols, _v._distance | *> FROM (<source>) _v
|
|
208
|
+
* [WHERE _distance <= ? [AND (<residual>)]]
|
|
209
|
+
* ORDER BY _distance ASC LIMIT ? [OFFSET ?]
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* `$select` projects the OUTER query exactly like `buildSelect` does
|
|
213
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`), so a
|
|
214
|
+
* residual filter still sees every source column; `_distance` is always
|
|
215
|
+
* returned. `maxDistance` is the threshold on the engine's distance scale;
|
|
216
|
+
* a `residual` filter references the source's columns qualified by `_v`.
|
|
217
|
+
* Placeholders are finalized for the dialect.
|
|
218
|
+
* @since 0.1.143
|
|
219
|
+
*/
|
|
220
|
+
declare function buildVectorSearchSelect(dialect: SqlDialect, source: TSqlFragment, opts: {
|
|
221
|
+
select?: UniquSelect;
|
|
222
|
+
limit: number;
|
|
223
|
+
skip?: number;
|
|
224
|
+
maxDistance?: number;
|
|
225
|
+
residual?: TSqlFragment;
|
|
226
|
+
}): TSqlFragment;
|
|
227
|
+
/**
|
|
228
|
+
* Count companion for {@link buildVectorSearchSelect} — rows the search
|
|
229
|
+
* could return (distance cap and residual applied, pagination ignored).
|
|
230
|
+
* Returns one row: `{ cnt }`.
|
|
231
|
+
* @since 0.1.143
|
|
232
|
+
*/
|
|
233
|
+
declare function buildVectorSearchCount(dialect: SqlDialect, source: TSqlFragment, opts: {
|
|
234
|
+
maxDistance?: number;
|
|
235
|
+
residual?: TSqlFragment;
|
|
236
|
+
}): TSqlFragment;
|
|
237
|
+
//#endregion
|
|
180
238
|
//#region src/view-builder.d.ts
|
|
181
239
|
/**
|
|
182
240
|
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
@@ -286,8 +344,12 @@ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFrag
|
|
|
286
344
|
declare function derivedColumnExpr(dialect: SqlDialect, field: TDbFieldMeta): string;
|
|
287
345
|
/**
|
|
288
346
|
* Builds a column projection (SELECT clause fields).
|
|
347
|
+
*
|
|
348
|
+
* @param qualifier - optional table alias every column (and the `*`
|
|
349
|
+
* fallback) is qualified with (`"t"."col"`), for projections over a joined
|
|
350
|
+
* or aliased source such as the geo / vector search subqueries (since 0.1.143).
|
|
289
351
|
*/
|
|
290
|
-
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
352
|
+
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect, qualifier?: string): string;
|
|
291
353
|
//#endregion
|
|
292
354
|
//#region src/common.d.ts
|
|
293
355
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
@@ -384,4 +446,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
384
446
|
flags: string;
|
|
385
447
|
};
|
|
386
448
|
//#endregion
|
|
387
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
|
449
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
|
package/dist/index.mjs
CHANGED
|
@@ -172,113 +172,6 @@ function buildWhere(dialect, filter) {
|
|
|
172
172
|
return walkFilter(filter, getVisitor(dialect)) ?? EMPTY_AND;
|
|
173
173
|
}
|
|
174
174
|
//#endregion
|
|
175
|
-
//#region src/geo.ts
|
|
176
|
-
/**
|
|
177
|
-
* Internal alias for the computed distance column in geo search queries.
|
|
178
|
-
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
179
|
-
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
180
|
-
* across dialects, so the public name can't be used directly.
|
|
181
|
-
*/
|
|
182
|
-
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
183
|
-
/**
|
|
184
|
-
* Builds a distance-ranked geo search SELECT:
|
|
185
|
-
*
|
|
186
|
-
* ```sql
|
|
187
|
-
* SELECT * FROM (
|
|
188
|
-
* SELECT t.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
189
|
-
* ) _g
|
|
190
|
-
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
191
|
-
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
192
|
-
* ```
|
|
193
|
-
*
|
|
194
|
-
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
195
|
-
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
196
|
-
* Placeholders stay `?`-style; callers finalize for `$N` dialects.
|
|
197
|
-
*/
|
|
198
|
-
function buildGeoSearchSelect(dialect, table, where, distExpr, window, controls) {
|
|
199
|
-
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
200
|
-
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`;
|
|
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
|
-
sql += ` ORDER BY ${alias} ASC`;
|
|
211
|
-
if (controls.$limit !== void 0) {
|
|
212
|
-
sql += ` LIMIT ?`;
|
|
213
|
-
params.push(controls.$limit);
|
|
214
|
-
}
|
|
215
|
-
if (controls.$skip !== void 0) {
|
|
216
|
-
if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
217
|
-
sql += ` OFFSET ?`;
|
|
218
|
-
params.push(controls.$skip);
|
|
219
|
-
}
|
|
220
|
-
return finalizeParams(dialect, {
|
|
221
|
-
sql,
|
|
222
|
-
params
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
227
|
-
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
228
|
-
*/
|
|
229
|
-
function buildGeoSearchCount(dialect, table, where, distExpr, window) {
|
|
230
|
-
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
231
|
-
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`;
|
|
232
|
-
const params = [...distExpr.params, ...where.params];
|
|
233
|
-
if (window.maxDistance !== void 0) {
|
|
234
|
-
sql += ` AND ${alias} <= ?`;
|
|
235
|
-
params.push(window.maxDistance);
|
|
236
|
-
}
|
|
237
|
-
if (window.minDistance !== void 0) {
|
|
238
|
-
sql += ` AND ${alias} >= ?`;
|
|
239
|
-
params.push(window.minDistance);
|
|
240
|
-
}
|
|
241
|
-
return finalizeParams(dialect, {
|
|
242
|
-
sql,
|
|
243
|
-
params
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
247
|
-
function geoWindowFromControls(controls) {
|
|
248
|
-
return {
|
|
249
|
-
maxDistance: typeof controls?.$maxDistance === "number" ? controls.$maxDistance : void 0,
|
|
250
|
-
minDistance: typeof controls?.$minDistance === "number" ? controls.$minDistance : void 0
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
255
|
-
* the raw tuple or its JSON-string form (the relational field mapper
|
|
256
|
-
* stringifies `storage: json` fields before adapter formatters run).
|
|
257
|
-
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
258
|
-
* which must pass through untouched).
|
|
259
|
-
*/
|
|
260
|
-
function normalizeGeoPointValue(value) {
|
|
261
|
-
let candidate = value;
|
|
262
|
-
if (typeof candidate === "string") {
|
|
263
|
-
if (!candidate.startsWith("[")) return;
|
|
264
|
-
try {
|
|
265
|
-
candidate = JSON.parse(candidate);
|
|
266
|
-
} catch {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (Array.isArray(candidate) && candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") return [candidate[0], candidate[1]];
|
|
271
|
-
}
|
|
272
|
-
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
273
|
-
function renameGeoDistance(row) {
|
|
274
|
-
if ("__atscript_distance" in row) {
|
|
275
|
-
const distance = row[GEO_DISTANCE_ALIAS];
|
|
276
|
-
delete row[GEO_DISTANCE_ALIAS];
|
|
277
|
-
row.$distance = typeof distance === "string" ? Number(distance) : distance;
|
|
278
|
-
}
|
|
279
|
-
return row;
|
|
280
|
-
}
|
|
281
|
-
//#endregion
|
|
282
175
|
//#region src/common.ts
|
|
283
176
|
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
284
177
|
function sqlStringLiteral(value) {
|
|
@@ -890,16 +783,223 @@ function derivedColumnExpr(dialect, field) {
|
|
|
890
783
|
}
|
|
891
784
|
/**
|
|
892
785
|
* Builds a column projection (SELECT clause fields).
|
|
786
|
+
*
|
|
787
|
+
* @param qualifier - optional table alias every column (and the `*`
|
|
788
|
+
* fallback) is qualified with (`"t"."col"`), for projections over a joined
|
|
789
|
+
* or aliased source such as the geo / vector search subqueries (since 0.1.143).
|
|
893
790
|
*/
|
|
894
|
-
function buildProjection(dialect, select) {
|
|
791
|
+
function buildProjection(dialect, select, qualifier) {
|
|
792
|
+
const prefix = qualifier === void 0 ? "" : `${dialect.quoteTable(qualifier)}.`;
|
|
895
793
|
const fields = select?.asArray;
|
|
896
|
-
if (!fields) return
|
|
794
|
+
if (!fields) return `${prefix}*`;
|
|
897
795
|
let sql = "";
|
|
898
796
|
for (let i = 0; i < fields.length; i++) {
|
|
899
797
|
if (i > 0) sql += ", ";
|
|
900
|
-
sql += dialect.quoteIdentifier(fields[i]);
|
|
798
|
+
sql += prefix + dialect.quoteIdentifier(fields[i]);
|
|
799
|
+
}
|
|
800
|
+
return sql || `${prefix}*`;
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/geo.ts
|
|
804
|
+
/**
|
|
805
|
+
* Internal alias for the computed distance column in geo search queries.
|
|
806
|
+
* Renamed to the public `$distance` pseudo-field after fetch (same convention
|
|
807
|
+
* as the MongoDB adapter) — SQL identifiers starting with `$` are awkward
|
|
808
|
+
* across dialects, so the public name can't be used directly.
|
|
809
|
+
*/
|
|
810
|
+
const GEO_DISTANCE_ALIAS = "__atscript_distance";
|
|
811
|
+
/**
|
|
812
|
+
* Builds a distance-ranked geo search SELECT:
|
|
813
|
+
*
|
|
814
|
+
* ```sql
|
|
815
|
+
* SELECT * FROM (
|
|
816
|
+
* SELECT <t.cols | t.*>, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
|
|
817
|
+
* ) _g
|
|
818
|
+
* WHERE __atscript_distance IS NOT NULL [AND <= ?] [AND >= ?]
|
|
819
|
+
* ORDER BY __atscript_distance ASC LIMIT ? [OFFSET ?]
|
|
820
|
+
* ```
|
|
821
|
+
*
|
|
822
|
+
* `distExpr` computes meters from the query point to the geo column (NULL for
|
|
823
|
+
* rows without a point — those are excluded, matching MongoDB `$geoNear`).
|
|
824
|
+
* `controls.$select` projects the row columns exactly like `buildSelect` does
|
|
825
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`); the
|
|
826
|
+
* distance column is always returned. Placeholders stay `?`-style; callers
|
|
827
|
+
* finalize for `$N` dialects.
|
|
828
|
+
*/
|
|
829
|
+
function buildGeoSearchSelect(dialect, table, where, distExpr, window, controls) {
|
|
830
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
831
|
+
let sql = `SELECT * FROM (${`SELECT ${buildProjection(dialect, controls.$select, "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`;
|
|
832
|
+
const params = [...distExpr.params, ...where.params];
|
|
833
|
+
if (window.maxDistance !== void 0) {
|
|
834
|
+
sql += ` AND ${alias} <= ?`;
|
|
835
|
+
params.push(window.maxDistance);
|
|
836
|
+
}
|
|
837
|
+
if (window.minDistance !== void 0) {
|
|
838
|
+
sql += ` AND ${alias} >= ?`;
|
|
839
|
+
params.push(window.minDistance);
|
|
840
|
+
}
|
|
841
|
+
sql += ` ORDER BY ${alias} ASC`;
|
|
842
|
+
if (controls.$limit !== void 0) {
|
|
843
|
+
sql += ` LIMIT ?`;
|
|
844
|
+
params.push(controls.$limit);
|
|
845
|
+
}
|
|
846
|
+
if (controls.$skip !== void 0) {
|
|
847
|
+
if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
848
|
+
sql += ` OFFSET ?`;
|
|
849
|
+
params.push(controls.$skip);
|
|
850
|
+
}
|
|
851
|
+
return finalizeParams(dialect, {
|
|
852
|
+
sql,
|
|
853
|
+
params
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Count companion for {@link buildGeoSearchSelect} — rows inside the distance
|
|
858
|
+
* window (filter applied, pagination ignored). Returns one row: `{ cnt }`.
|
|
859
|
+
*/
|
|
860
|
+
function buildGeoSearchCount(dialect, table, where, distExpr, window) {
|
|
861
|
+
const alias = dialect.quoteIdentifier(GEO_DISTANCE_ALIAS);
|
|
862
|
+
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`;
|
|
863
|
+
const params = [...distExpr.params, ...where.params];
|
|
864
|
+
if (window.maxDistance !== void 0) {
|
|
865
|
+
sql += ` AND ${alias} <= ?`;
|
|
866
|
+
params.push(window.maxDistance);
|
|
867
|
+
}
|
|
868
|
+
if (window.minDistance !== void 0) {
|
|
869
|
+
sql += ` AND ${alias} >= ?`;
|
|
870
|
+
params.push(window.minDistance);
|
|
871
|
+
}
|
|
872
|
+
return finalizeParams(dialect, {
|
|
873
|
+
sql,
|
|
874
|
+
params
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/** Extracts the `$maxDistance` / `$minDistance` window from query controls. */
|
|
878
|
+
function geoWindowFromControls(controls) {
|
|
879
|
+
return {
|
|
880
|
+
maxDistance: typeof controls?.$maxDistance === "number" ? controls.$maxDistance : void 0,
|
|
881
|
+
minDistance: typeof controls?.$minDistance === "number" ? controls.$minDistance : void 0
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Normalizes a write-path geo value to a `[lng, lat]` tuple. The value may be
|
|
886
|
+
* the raw tuple or its JSON-string form (the relational field mapper
|
|
887
|
+
* stringifies `storage: json` fields before adapter formatters run).
|
|
888
|
+
* Returns `undefined` for anything else (e.g. `$geoWithin` circle objects,
|
|
889
|
+
* which must pass through untouched).
|
|
890
|
+
*/
|
|
891
|
+
function normalizeGeoPointValue(value) {
|
|
892
|
+
let candidate = value;
|
|
893
|
+
if (typeof candidate === "string") {
|
|
894
|
+
if (!candidate.startsWith("[")) return;
|
|
895
|
+
try {
|
|
896
|
+
candidate = JSON.parse(candidate);
|
|
897
|
+
} catch {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
901
900
|
}
|
|
902
|
-
|
|
901
|
+
if (Array.isArray(candidate) && candidate.length === 2 && typeof candidate[0] === "number" && typeof candidate[1] === "number") return [candidate[0], candidate[1]];
|
|
902
|
+
}
|
|
903
|
+
/** Renames the internal distance alias to the public `$distance` field (in place). */
|
|
904
|
+
function renameGeoDistance(row) {
|
|
905
|
+
if ("__atscript_distance" in row) {
|
|
906
|
+
const distance = row[GEO_DISTANCE_ALIAS];
|
|
907
|
+
delete row[GEO_DISTANCE_ALIAS];
|
|
908
|
+
row.$distance = typeof distance === "string" ? Number(distance) : distance;
|
|
909
|
+
}
|
|
910
|
+
return row;
|
|
911
|
+
}
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/vector.ts
|
|
914
|
+
/** Column every vector search row carries: the engine's distance to the query vector. */
|
|
915
|
+
const VECTOR_DISTANCE_ALIAS = "_distance";
|
|
916
|
+
/** Alias of the row source inside the vector search queries. */
|
|
917
|
+
const SOURCE_ALIAS = "_v";
|
|
918
|
+
/**
|
|
919
|
+
* The row source of a vector search over a plain table:
|
|
920
|
+
*
|
|
921
|
+
* ```sql
|
|
922
|
+
* SELECT t.*, <distExpr> AS _distance FROM <table> t WHERE <where>
|
|
923
|
+
* ```
|
|
924
|
+
*
|
|
925
|
+
* `withRows: false` selects the distance only (the count companion).
|
|
926
|
+
* Placeholders stay `?`-style.
|
|
927
|
+
* @since 0.1.143
|
|
928
|
+
*/
|
|
929
|
+
function vectorDistanceSource(dialect, table, where, distExpr, withRows = true) {
|
|
930
|
+
const t = dialect.quoteTable("t");
|
|
931
|
+
return {
|
|
932
|
+
sql: `SELECT ${withRows ? `${t}.*, ` : ""}${distExpr.sql} AS ${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)} FROM ${dialect.quoteTable(table)} AS ${t} WHERE ${where.sql}`,
|
|
933
|
+
params: [...distExpr.params, ...where.params]
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
/** The outer WHERE of a vector search: the distance cap, then a residual filter over the source. */
|
|
937
|
+
function outerWhere(dialect, maxDistance, residual) {
|
|
938
|
+
const parts = [];
|
|
939
|
+
const params = [];
|
|
940
|
+
if (maxDistance !== void 0) {
|
|
941
|
+
parts.push(`${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)} <= ?`);
|
|
942
|
+
params.push(maxDistance);
|
|
943
|
+
}
|
|
944
|
+
if (residual && residual.sql !== "1=1") {
|
|
945
|
+
parts.push(`(${residual.sql})`);
|
|
946
|
+
params.push(...residual.params);
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
sql: parts.length > 0 ? ` WHERE ${parts.join(" AND ")}` : "",
|
|
950
|
+
params
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Builds a distance-ranked vector search SELECT over a row source that
|
|
955
|
+
* carries a `_distance` column ({@link vectorDistanceSource}, or an
|
|
956
|
+
* engine-specific one such as a vector-index join):
|
|
957
|
+
*
|
|
958
|
+
* ```sql
|
|
959
|
+
* SELECT <_v.cols, _v._distance | *> FROM (<source>) _v
|
|
960
|
+
* [WHERE _distance <= ? [AND (<residual>)]]
|
|
961
|
+
* ORDER BY _distance ASC LIMIT ? [OFFSET ?]
|
|
962
|
+
* ```
|
|
963
|
+
*
|
|
964
|
+
* `$select` projects the OUTER query exactly like `buildSelect` does
|
|
965
|
+
* (inclusion and exclusion forms, resolved by `UniquSelect.asArray`), so a
|
|
966
|
+
* residual filter still sees every source column; `_distance` is always
|
|
967
|
+
* returned. `maxDistance` is the threshold on the engine's distance scale;
|
|
968
|
+
* a `residual` filter references the source's columns qualified by `_v`.
|
|
969
|
+
* Placeholders are finalized for the dialect.
|
|
970
|
+
* @since 0.1.143
|
|
971
|
+
*/
|
|
972
|
+
function buildVectorSearchSelect(dialect, source, opts) {
|
|
973
|
+
const v = dialect.quoteTable(SOURCE_ALIAS);
|
|
974
|
+
const cols = opts.select?.asArray?.length ? `${buildProjection(dialect, opts.select, SOURCE_ALIAS)}, ${v}.${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)}` : "*";
|
|
975
|
+
const where = outerWhere(dialect, opts.maxDistance, opts.residual);
|
|
976
|
+
let sql = `SELECT ${cols} FROM (${source.sql}) AS ${v}${where.sql} ORDER BY ${dialect.quoteIdentifier(VECTOR_DISTANCE_ALIAS)} ASC LIMIT ?`;
|
|
977
|
+
const params = [
|
|
978
|
+
...source.params,
|
|
979
|
+
...where.params,
|
|
980
|
+
opts.limit
|
|
981
|
+
];
|
|
982
|
+
if (opts.skip) {
|
|
983
|
+
sql += ` OFFSET ?`;
|
|
984
|
+
params.push(opts.skip);
|
|
985
|
+
}
|
|
986
|
+
return finalizeParams(dialect, {
|
|
987
|
+
sql,
|
|
988
|
+
params
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Count companion for {@link buildVectorSearchSelect} — rows the search
|
|
993
|
+
* could return (distance cap and residual applied, pagination ignored).
|
|
994
|
+
* Returns one row: `{ cnt }`.
|
|
995
|
+
* @since 0.1.143
|
|
996
|
+
*/
|
|
997
|
+
function buildVectorSearchCount(dialect, source, opts) {
|
|
998
|
+
const where = outerWhere(dialect, opts.maxDistance, opts.residual);
|
|
999
|
+
return finalizeParams(dialect, {
|
|
1000
|
+
sql: `SELECT COUNT(*) AS cnt FROM (${source.sql}) AS ${dialect.quoteTable(SOURCE_ALIAS)}${where.sql}`,
|
|
1001
|
+
params: [...source.params, ...where.params]
|
|
1002
|
+
});
|
|
903
1003
|
}
|
|
904
1004
|
//#endregion
|
|
905
1005
|
//#region src/regex.ts
|
|
@@ -927,4 +1027,4 @@ function parseRegexString(value) {
|
|
|
927
1027
|
};
|
|
928
1028
|
}
|
|
929
1029
|
//#endregion
|
|
930
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue };
|
|
1030
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, toSqlValue, vectorDistanceSource };
|
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.143",
|
|
4
4
|
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@uniqu/core": "^0.1.11",
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
45
|
+
"@atscript/db": "^0.1.143"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|