@atscript/db-sqlite 0.1.105 → 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 +112 -10
- package/dist/index.d.cts +21 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +113 -11
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.106",
|
|
4
4
|
"description": "SQLite adapter for @atscript/db with swappable driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
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.
|
|
53
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
52
|
+
"@atscript/db": "^0.1.106",
|
|
53
|
+
"@atscript/db-sql-tools": "^0.1.106"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"better-sqlite3": {
|