@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 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
- return sql || "*";
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.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
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
- * Placeholders stay `?`-style; callers finalize for `$N` dialects.
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.*, <distExpr> AS __atscript_distance FROM <table> t WHERE <filter>
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
- * Placeholders stay `?`-style; callers finalize for `$N` dialects.
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
- return sql || "*";
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.141",
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.141"
45
+ "@atscript/db": "^0.1.143"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "vp pack",