@atscript/db-sqlite 0.1.104 → 0.1.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -180,9 +180,34 @@ const sqliteDialect = {
180
180
  params: [likePattern]
181
181
  };
182
182
  },
183
+ geoWithin(quotedCol, circle) {
184
+ const dist = haversineDistanceExpr(quotedCol, circle.center);
185
+ return {
186
+ sql: `${dist.sql} <= ?`,
187
+ params: [...dist.params, circle.radius]
188
+ };
189
+ },
183
190
  createViewPrefix: "CREATE VIEW IF NOT EXISTS"
184
191
  };
185
192
  /**
193
+ * Great-circle distance in meters from a JSON-stored `[lng, lat]` column to a
194
+ * query point, via the haversine formula (mean earth radius 6 371 000 m).
195
+ * NULL columns yield NULL — geo search excludes them, matching `$geoNear`.
196
+ * Needs SQLite math functions (`SQLITE_ENABLE_MATH_FUNCTIONS`, on by default
197
+ * in better-sqlite3 builds).
198
+ */
199
+ function haversineDistanceExpr(quotedCol, point) {
200
+ const lat = `radians(json_extract(${quotedCol}, '$[1]'))`;
201
+ return {
202
+ sql: `12742000.0 * asin(min(1.0, sqrt(pow(sin((${lat} - radians(?)) / 2.0), 2) + cos(radians(?)) * cos(${lat}) * pow(sin((${`radians(json_extract(${quotedCol}, '$[0]'))`} - radians(?)) / 2.0), 2))))`,
203
+ params: [
204
+ point[1],
205
+ point[1],
206
+ point[0]
207
+ ]
208
+ };
209
+ }
210
+ /**
186
211
  * Builds an INSERT statement.
187
212
  *
188
213
  * @param table - Table name.
@@ -348,6 +373,8 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
348
373
  }
349
374
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
350
375
  _supportsVector;
376
+ /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
377
+ _supportsMathFns;
351
378
  /** Vector fields: field path → { dimensions, similarity, indexName }. */
352
379
  _vectorFields = /* @__PURE__ */ new Map();
353
380
  /** Default similarity thresholds per vector index (from @db.search.vector.threshold). */
@@ -703,6 +730,21 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
703
730
  }
704
731
  });
705
732
  }
733
+ async dropIndexesForColumns(columns) {
734
+ const tableName = this.resolveTableName();
735
+ const dropped = new Set(columns);
736
+ const indexes = this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => i.name.startsWith("atscript__"));
737
+ for (const index of indexes) if (this.driver.all(`PRAGMA index_info("${esc(index.name)}")`).some((c) => c.name !== null && dropped.has(c.name))) {
738
+ const sql = `DROP INDEX IF EXISTS "${esc(index.name)}"`;
739
+ this._log(sql);
740
+ this.driver.exec(sql);
741
+ }
742
+ for (const name of this._listShadowTables(tableName, "fts")) if (this.driver.all(`PRAGMA table_info("${esc(name)}")`).some((c) => dropped.has(c.name))) this._dropFtsTable(name);
743
+ for (const name of this._listShadowTables(tableName, "vec")) {
744
+ const triggerSql = this.driver.all(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name = ?`, [`${name}__ai`])[0]?.sql ?? "";
745
+ if (columns.some((c) => triggerSql.includes(`"${esc(c)}"`))) this._dropVecTable(name);
746
+ }
747
+ }
706
748
  async dropTableByName(tableName) {
707
749
  this._dropAllFtsTables(tableName);
708
750
  this._dropAllVecTables(tableName);
@@ -739,7 +781,10 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
739
781
  async syncIndexes() {
740
782
  const tableName = this.resolveTableName();
741
783
  await this.syncIndexesWithDiff({
742
- listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")),
784
+ listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
785
+ name: i.name,
786
+ columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
787
+ })),
743
788
  createIndex: async (index) => {
744
789
  const unique = index.type === "unique" ? "UNIQUE " : "";
745
790
  const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
@@ -752,11 +797,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
752
797
  this._log(sql);
753
798
  this.driver.exec(sql);
754
799
  },
755
- shouldSkipType: (type) => type === "fulltext",
756
- warnUnsupportedTypes: {
757
- adapter: "sqlite",
758
- types: ["geo"]
759
- }
800
+ shouldSkipType: (type) => type === "fulltext" || type === "geo"
760
801
  });
761
802
  this._syncFtsIndexes(tableName);
762
803
  this._syncVecIndexes(tableName);
@@ -904,10 +945,13 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
904
945
  this._log(sql);
905
946
  this.driver.exec(sql);
906
947
  }
948
+ /** Lists FTS5/vec0 shadow virtual tables for a content table. */
949
+ _listShadowTables(tableName, kind) {
950
+ return this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__${kind}__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
951
+ }
907
952
  /** Drops all FTS virtual tables and triggers for a content table. */
908
953
  _dropAllFtsTables(tableName) {
909
- const ftsTables = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__fts__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE"));
910
- for (const { name } of ftsTables) this._dropFtsTable(name);
954
+ for (const name of this._listShadowTables(tableName, "fts")) this._dropFtsTable(name);
911
955
  }
912
956
  static _RESIDUAL_OVERFETCH = 4;
913
957
  async vectorSearch(vector, query, indexName) {
@@ -1033,6 +1077,65 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1033
1077
  skip
1034
1078
  };
1035
1079
  }
1080
+ /**
1081
+ * Geo search is available when SQLite has math functions
1082
+ * (`SQLITE_ENABLE_MATH_FUNCTIONS` — on by default in better-sqlite3 builds).
1083
+ * Distance is computed per row via haversine; declared geo indexes have no
1084
+ * physical artifact on SQLite.
1085
+ */
1086
+ isGeoSearchable() {
1087
+ return this._detectMathFns();
1088
+ }
1089
+ _detectMathFns() {
1090
+ if (this._supportsMathFns === void 0) try {
1091
+ this.driver.get("SELECT radians(0) AS r");
1092
+ this._supportsMathFns = true;
1093
+ } catch {
1094
+ this._supportsMathFns = false;
1095
+ this._log("[atscript-db-sqlite] SQLite math functions unavailable — geo search disabled (rebuild SQLite with SQLITE_ENABLE_MATH_FUNCTIONS).");
1096
+ }
1097
+ return this._supportsMathFns;
1098
+ }
1099
+ async geoSearch(point, query, indexName) {
1100
+ const { sql, params } = this._buildGeoSearchSelect(this._prepareGeoSearch(point, query, indexName));
1101
+ this._log(sql, params);
1102
+ return this.driver.all(sql, params).map((row) => (0, _atscript_db_sql_tools.renameGeoDistance)(row));
1103
+ }
1104
+ async geoSearchWithCount(point, query, indexName) {
1105
+ const ctx = this._prepareGeoSearch(point, query, indexName);
1106
+ const { sql, params } = this._buildGeoSearchSelect(ctx);
1107
+ const countFrag = (0, _atscript_db_sql_tools.buildGeoSearchCount)(sqliteDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window);
1108
+ this._log(sql, params);
1109
+ this._log(countFrag.sql, countFrag.params);
1110
+ const rows = this.driver.all(sql, params);
1111
+ const countRow = this.driver.get(countFrag.sql, countFrag.params);
1112
+ return {
1113
+ data: rows.map((row) => (0, _atscript_db_sql_tools.renameGeoDistance)(row)),
1114
+ count: countRow?.cnt ?? 0
1115
+ };
1116
+ }
1117
+ /** Resolves the shared parts of a geo search once (column, filter, distance, window). */
1118
+ _prepareGeoSearch(point, query, indexName) {
1119
+ if (!this._detectMathFns()) throw new _atscript_db.DbError("GEO_NOT_SUPPORTED", [{
1120
+ path: "",
1121
+ message: "Geo search requires SQLite math functions"
1122
+ }]);
1123
+ const column = this._resolveGeoColumn(indexName);
1124
+ const controls = query.controls ?? {};
1125
+ return {
1126
+ tableName: this.resolveTableName(),
1127
+ where: buildWhere(query.filter),
1128
+ dist: haversineDistanceExpr(`"${esc(column)}"`, point),
1129
+ window: (0, _atscript_db_sql_tools.geoWindowFromControls)(controls),
1130
+ controls
1131
+ };
1132
+ }
1133
+ _buildGeoSearchSelect(ctx) {
1134
+ return (0, _atscript_db_sql_tools.buildGeoSearchSelect)(sqliteDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window, {
1135
+ $limit: ctx.controls.$limit,
1136
+ $skip: ctx.controls.$skip
1137
+ });
1138
+ }
1036
1139
  /** Builds vec0 shadow table name from index name: `<table>__vec__<indexName>`. */
1037
1140
  _vecTableName(indexName) {
1038
1141
  return `${this.resolveTableName()}__vec__${indexName}`;
@@ -1125,8 +1228,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1125
1228
  }
1126
1229
  /** Drops all vec0 virtual shadow tables and triggers for a content table. */
1127
1230
  _dropAllVecTables(tableName) {
1128
- const vecTables = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__vec__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE"));
1129
- for (const { name } of vecTables) this._dropVecTable(name);
1231
+ for (const name of this._listShadowTables(tableName, "vec")) this._dropVecTable(name);
1130
1232
  }
1131
1233
  };
1132
1234
  /** Normalizes SQLite PRAGMA dflt_value to match serialized format.
package/dist/index.d.cts CHANGED
@@ -75,6 +75,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
75
75
  supportsNativeValueDefaults(): boolean;
76
76
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
77
77
  private _supportsVector;
78
+ /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
79
+ private _supportsMathFns;
78
80
  /** Vector fields: field path → { dimensions, similarity, indexName }. */
79
81
  private _vectorFields;
80
82
  /** Default similarity thresholds per vector index (from @db.search.vector.threshold). */
@@ -124,6 +126,7 @@ declare class SqliteAdapter extends BaseDbAdapter {
124
126
  recreateTable(): Promise<void>;
125
127
  dropTable(): Promise<void>;
126
128
  dropColumns(columns: string[]): Promise<void>;
129
+ dropIndexesForColumns(columns: string[]): Promise<void>;
127
130
  dropTableByName(tableName: string): Promise<void>;
128
131
  dropViewByName(viewName: string): Promise<void>;
129
132
  renameTable(oldName: string): Promise<void>;
@@ -155,6 +158,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
155
158
  private _createFtsTable;
156
159
  /** Drops an FTS5 virtual table and its sync triggers. */
157
160
  private _dropFtsTable;
161
+ /** Lists FTS5/vec0 shadow virtual tables for a content table. */
162
+ private _listShadowTables;
158
163
  /** Drops all FTS virtual tables and triggers for a content table. */
159
164
  private _dropAllFtsTables;
160
165
  private static readonly _RESIDUAL_OVERFETCH;
@@ -179,6 +184,22 @@ declare class SqliteAdapter extends BaseDbAdapter {
179
184
  * ORDER BY + LIMIT/OFFSET, the latter wraps it in a COUNT(*).
180
185
  */
181
186
  private _buildVectorSearchBase;
187
+ /**
188
+ * Geo search is available when SQLite has math functions
189
+ * (`SQLITE_ENABLE_MATH_FUNCTIONS` — on by default in better-sqlite3 builds).
190
+ * Distance is computed per row via haversine; declared geo indexes have no
191
+ * physical artifact on SQLite.
192
+ */
193
+ isGeoSearchable(): boolean;
194
+ private _detectMathFns;
195
+ geoSearch(point: [number, number], query: DbQuery, indexName?: string): Promise<Array<Record<string, unknown>>>;
196
+ geoSearchWithCount(point: [number, number], query: DbQuery, indexName?: string): Promise<{
197
+ data: Array<Record<string, unknown>>;
198
+ count: number;
199
+ }>;
200
+ /** Resolves the shared parts of a geo search once (column, filter, distance, window). */
201
+ private _prepareGeoSearch;
202
+ private _buildGeoSearchSelect;
182
203
  /** Builds vec0 shadow table name from index name: `<table>__vec__<indexName>`. */
183
204
  private _vecTableName;
184
205
  /**
package/dist/index.d.mts CHANGED
@@ -75,6 +75,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
75
75
  supportsNativeValueDefaults(): boolean;
76
76
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
77
77
  private _supportsVector;
78
+ /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
79
+ private _supportsMathFns;
78
80
  /** Vector fields: field path → { dimensions, similarity, indexName }. */
79
81
  private _vectorFields;
80
82
  /** Default similarity thresholds per vector index (from @db.search.vector.threshold). */
@@ -124,6 +126,7 @@ declare class SqliteAdapter extends BaseDbAdapter {
124
126
  recreateTable(): Promise<void>;
125
127
  dropTable(): Promise<void>;
126
128
  dropColumns(columns: string[]): Promise<void>;
129
+ dropIndexesForColumns(columns: string[]): Promise<void>;
127
130
  dropTableByName(tableName: string): Promise<void>;
128
131
  dropViewByName(viewName: string): Promise<void>;
129
132
  renameTable(oldName: string): Promise<void>;
@@ -155,6 +158,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
155
158
  private _createFtsTable;
156
159
  /** Drops an FTS5 virtual table and its sync triggers. */
157
160
  private _dropFtsTable;
161
+ /** Lists FTS5/vec0 shadow virtual tables for a content table. */
162
+ private _listShadowTables;
158
163
  /** Drops all FTS virtual tables and triggers for a content table. */
159
164
  private _dropAllFtsTables;
160
165
  private static readonly _RESIDUAL_OVERFETCH;
@@ -179,6 +184,22 @@ declare class SqliteAdapter extends BaseDbAdapter {
179
184
  * ORDER BY + LIMIT/OFFSET, the latter wraps it in a COUNT(*).
180
185
  */
181
186
  private _buildVectorSearchBase;
187
+ /**
188
+ * Geo search is available when SQLite has math functions
189
+ * (`SQLITE_ENABLE_MATH_FUNCTIONS` — on by default in better-sqlite3 builds).
190
+ * Distance is computed per row via haversine; declared geo indexes have no
191
+ * physical artifact on SQLite.
192
+ */
193
+ isGeoSearchable(): boolean;
194
+ private _detectMathFns;
195
+ geoSearch(point: [number, number], query: DbQuery, indexName?: string): Promise<Array<Record<string, unknown>>>;
196
+ geoSearchWithCount(point: [number, number], query: DbQuery, indexName?: string): Promise<{
197
+ data: Array<Record<string, unknown>>;
198
+ count: number;
199
+ }>;
200
+ /** Resolves the shared parts of a geo search once (column, filter, distance, window). */
201
+ private _prepareGeoSearch;
202
+ private _buildGeoSearchSelect;
182
203
  /** Builds vec0 shadow table name from index name: `<table>__vec__<indexName>`. */
183
204
  private _vecTableName;
184
205
  /**
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
3
- import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, parseRegexString, refActionToSql, toSqlValue } from "@atscript/db-sql-tools";
3
+ import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, geoWindowFromControls, parseRegexString, refActionToSql, renameGeoDistance, toSqlValue } from "@atscript/db-sql-tools";
4
4
  //#region src/better-sqlite3-driver.ts
5
5
  /**
6
6
  * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
@@ -179,9 +179,34 @@ const sqliteDialect = {
179
179
  params: [likePattern]
180
180
  };
181
181
  },
182
+ geoWithin(quotedCol, circle) {
183
+ const dist = haversineDistanceExpr(quotedCol, circle.center);
184
+ return {
185
+ sql: `${dist.sql} <= ?`,
186
+ params: [...dist.params, circle.radius]
187
+ };
188
+ },
182
189
  createViewPrefix: "CREATE VIEW IF NOT EXISTS"
183
190
  };
184
191
  /**
192
+ * Great-circle distance in meters from a JSON-stored `[lng, lat]` column to a
193
+ * query point, via the haversine formula (mean earth radius 6 371 000 m).
194
+ * NULL columns yield NULL — geo search excludes them, matching `$geoNear`.
195
+ * Needs SQLite math functions (`SQLITE_ENABLE_MATH_FUNCTIONS`, on by default
196
+ * in better-sqlite3 builds).
197
+ */
198
+ function haversineDistanceExpr(quotedCol, point) {
199
+ const lat = `radians(json_extract(${quotedCol}, '$[1]'))`;
200
+ return {
201
+ sql: `12742000.0 * asin(min(1.0, sqrt(pow(sin((${lat} - radians(?)) / 2.0), 2) + cos(radians(?)) * cos(${lat}) * pow(sin((${`radians(json_extract(${quotedCol}, '$[0]'))`} - radians(?)) / 2.0), 2))))`,
202
+ params: [
203
+ point[1],
204
+ point[1],
205
+ point[0]
206
+ ]
207
+ };
208
+ }
209
+ /**
185
210
  * Builds an INSERT statement.
186
211
  *
187
212
  * @param table - Table name.
@@ -347,6 +372,8 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
347
372
  }
348
373
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
349
374
  _supportsVector;
375
+ /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
376
+ _supportsMathFns;
350
377
  /** Vector fields: field path → { dimensions, similarity, indexName }. */
351
378
  _vectorFields = /* @__PURE__ */ new Map();
352
379
  /** Default similarity thresholds per vector index (from @db.search.vector.threshold). */
@@ -702,6 +729,21 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
702
729
  }
703
730
  });
704
731
  }
732
+ async dropIndexesForColumns(columns) {
733
+ const tableName = this.resolveTableName();
734
+ const dropped = new Set(columns);
735
+ const indexes = this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => i.name.startsWith("atscript__"));
736
+ for (const index of indexes) if (this.driver.all(`PRAGMA index_info("${esc(index.name)}")`).some((c) => c.name !== null && dropped.has(c.name))) {
737
+ const sql = `DROP INDEX IF EXISTS "${esc(index.name)}"`;
738
+ this._log(sql);
739
+ this.driver.exec(sql);
740
+ }
741
+ for (const name of this._listShadowTables(tableName, "fts")) if (this.driver.all(`PRAGMA table_info("${esc(name)}")`).some((c) => dropped.has(c.name))) this._dropFtsTable(name);
742
+ for (const name of this._listShadowTables(tableName, "vec")) {
743
+ const triggerSql = this.driver.all(`SELECT sql FROM sqlite_master WHERE type='trigger' AND name = ?`, [`${name}__ai`])[0]?.sql ?? "";
744
+ if (columns.some((c) => triggerSql.includes(`"${esc(c)}"`))) this._dropVecTable(name);
745
+ }
746
+ }
705
747
  async dropTableByName(tableName) {
706
748
  this._dropAllFtsTables(tableName);
707
749
  this._dropAllVecTables(tableName);
@@ -738,7 +780,10 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
738
780
  async syncIndexes() {
739
781
  const tableName = this.resolveTableName();
740
782
  await this.syncIndexesWithDiff({
741
- listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")),
783
+ listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
784
+ name: i.name,
785
+ columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
786
+ })),
742
787
  createIndex: async (index) => {
743
788
  const unique = index.type === "unique" ? "UNIQUE " : "";
744
789
  const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
@@ -751,11 +796,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
751
796
  this._log(sql);
752
797
  this.driver.exec(sql);
753
798
  },
754
- shouldSkipType: (type) => type === "fulltext",
755
- warnUnsupportedTypes: {
756
- adapter: "sqlite",
757
- types: ["geo"]
758
- }
799
+ shouldSkipType: (type) => type === "fulltext" || type === "geo"
759
800
  });
760
801
  this._syncFtsIndexes(tableName);
761
802
  this._syncVecIndexes(tableName);
@@ -903,10 +944,13 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
903
944
  this._log(sql);
904
945
  this.driver.exec(sql);
905
946
  }
947
+ /** Lists FTS5/vec0 shadow virtual tables for a content table. */
948
+ _listShadowTables(tableName, kind) {
949
+ return this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__${kind}__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
950
+ }
906
951
  /** Drops all FTS virtual tables and triggers for a content table. */
907
952
  _dropAllFtsTables(tableName) {
908
- const ftsTables = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__fts__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE"));
909
- for (const { name } of ftsTables) this._dropFtsTable(name);
953
+ for (const name of this._listShadowTables(tableName, "fts")) this._dropFtsTable(name);
910
954
  }
911
955
  static _RESIDUAL_OVERFETCH = 4;
912
956
  async vectorSearch(vector, query, indexName) {
@@ -1032,6 +1076,65 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1032
1076
  skip
1033
1077
  };
1034
1078
  }
1079
+ /**
1080
+ * Geo search is available when SQLite has math functions
1081
+ * (`SQLITE_ENABLE_MATH_FUNCTIONS` — on by default in better-sqlite3 builds).
1082
+ * Distance is computed per row via haversine; declared geo indexes have no
1083
+ * physical artifact on SQLite.
1084
+ */
1085
+ isGeoSearchable() {
1086
+ return this._detectMathFns();
1087
+ }
1088
+ _detectMathFns() {
1089
+ if (this._supportsMathFns === void 0) try {
1090
+ this.driver.get("SELECT radians(0) AS r");
1091
+ this._supportsMathFns = true;
1092
+ } catch {
1093
+ this._supportsMathFns = false;
1094
+ this._log("[atscript-db-sqlite] SQLite math functions unavailable — geo search disabled (rebuild SQLite with SQLITE_ENABLE_MATH_FUNCTIONS).");
1095
+ }
1096
+ return this._supportsMathFns;
1097
+ }
1098
+ async geoSearch(point, query, indexName) {
1099
+ const { sql, params } = this._buildGeoSearchSelect(this._prepareGeoSearch(point, query, indexName));
1100
+ this._log(sql, params);
1101
+ return this.driver.all(sql, params).map((row) => renameGeoDistance(row));
1102
+ }
1103
+ async geoSearchWithCount(point, query, indexName) {
1104
+ const ctx = this._prepareGeoSearch(point, query, indexName);
1105
+ const { sql, params } = this._buildGeoSearchSelect(ctx);
1106
+ const countFrag = buildGeoSearchCount(sqliteDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window);
1107
+ this._log(sql, params);
1108
+ this._log(countFrag.sql, countFrag.params);
1109
+ const rows = this.driver.all(sql, params);
1110
+ const countRow = this.driver.get(countFrag.sql, countFrag.params);
1111
+ return {
1112
+ data: rows.map((row) => renameGeoDistance(row)),
1113
+ count: countRow?.cnt ?? 0
1114
+ };
1115
+ }
1116
+ /** Resolves the shared parts of a geo search once (column, filter, distance, window). */
1117
+ _prepareGeoSearch(point, query, indexName) {
1118
+ if (!this._detectMathFns()) throw new DbError("GEO_NOT_SUPPORTED", [{
1119
+ path: "",
1120
+ message: "Geo search requires SQLite math functions"
1121
+ }]);
1122
+ const column = this._resolveGeoColumn(indexName);
1123
+ const controls = query.controls ?? {};
1124
+ return {
1125
+ tableName: this.resolveTableName(),
1126
+ where: buildWhere(query.filter),
1127
+ dist: haversineDistanceExpr(`"${esc(column)}"`, point),
1128
+ window: geoWindowFromControls(controls),
1129
+ controls
1130
+ };
1131
+ }
1132
+ _buildGeoSearchSelect(ctx) {
1133
+ return buildGeoSearchSelect(sqliteDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window, {
1134
+ $limit: ctx.controls.$limit,
1135
+ $skip: ctx.controls.$skip
1136
+ });
1137
+ }
1035
1138
  /** Builds vec0 shadow table name from index name: `<table>__vec__<indexName>`. */
1036
1139
  _vecTableName(indexName) {
1037
1140
  return `${this.resolveTableName()}__vec__${indexName}`;
@@ -1124,8 +1227,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1124
1227
  }
1125
1228
  /** Drops all vec0 virtual shadow tables and triggers for a content table. */
1126
1229
  _dropAllVecTables(tableName) {
1127
- const vecTables = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__vec__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE"));
1128
- for (const { name } of vecTables) this._dropVecTable(name);
1230
+ for (const name of this._listShadowTables(tableName, "vec")) this._dropVecTable(name);
1129
1231
  }
1130
1232
  };
1131
1233
  /** Normalizes SQLite PRAGMA dflt_value to match serialized format.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sqlite",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "SQLite adapter for @atscript/db with swappable driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -36,21 +36,21 @@
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
- "@atscript/core": "^0.1.75",
40
- "@atscript/typescript": "^0.1.75",
39
+ "@atscript/core": "^0.1.76",
40
+ "@atscript/typescript": "^0.1.76",
41
41
  "@types/better-sqlite3": "^7.6.13",
42
42
  "@uniqu/core": "^0.1.6",
43
43
  "better-sqlite3": "^12.6.2",
44
44
  "sqlite-vec": "^0.1.9",
45
- "unplugin-atscript": "^0.1.75"
45
+ "unplugin-atscript": "^0.1.76"
46
46
  },
47
47
  "peerDependencies": {
48
- "@atscript/core": "^0.1.75",
49
- "@atscript/typescript": "^0.1.75",
48
+ "@atscript/core": "^0.1.76",
49
+ "@atscript/typescript": "^0.1.76",
50
50
  "@uniqu/core": "^0.1.6",
51
51
  "better-sqlite3": ">=11.0.0",
52
- "@atscript/db": "^0.1.104",
53
- "@atscript/db-sql-tools": "^0.1.104"
52
+ "@atscript/db": "^0.1.106",
53
+ "@atscript/db-sql-tools": "^0.1.106"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "better-sqlite3": {