@atscript/db-sqlite 0.1.163 → 0.1.165
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 +105 -38
- package/dist/index.d.cts +14 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +107 -40
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -379,8 +379,8 @@ function haversineDistanceExpr(quotedCol, point) {
|
|
|
379
379
|
/**
|
|
380
380
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
381
381
|
*/
|
|
382
|
-
function buildSelect(table, where, controls) {
|
|
383
|
-
return (0, _atscript_db_sql_tools.buildSelect)(sqliteDialect, table, where, controls);
|
|
382
|
+
function buildSelect(table, where, controls, defaultOrder) {
|
|
383
|
+
return (0, _atscript_db_sql_tools.buildSelect)(sqliteDialect, table, where, controls, void 0, defaultOrder);
|
|
384
384
|
}
|
|
385
385
|
/**
|
|
386
386
|
* Builds an UPDATE ... SET ... WHERE statement.
|
|
@@ -1011,32 +1011,42 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1011
1011
|
* Wraps a write operation to catch native SQLite constraint errors
|
|
1012
1012
|
* and rethrow as structured `DbError`.
|
|
1013
1013
|
*/
|
|
1014
|
-
_wrapConstraintError(fn) {
|
|
1014
|
+
_wrapConstraintError(fn, row) {
|
|
1015
1015
|
try {
|
|
1016
1016
|
return fn();
|
|
1017
1017
|
} catch (error) {
|
|
1018
1018
|
if (error instanceof Error) {
|
|
1019
|
-
if (error.message.includes("FOREIGN KEY constraint failed"))
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
const
|
|
1024
|
-
if (
|
|
1025
|
-
path: uniqueMatch[1],
|
|
1026
|
-
message: error.message
|
|
1027
|
-
}]);
|
|
1028
|
-
if (error.message.includes("UNIQUE constraint failed")) throw new _atscript_db.DbError("CONFLICT", [{
|
|
1029
|
-
path: "",
|
|
1030
|
-
message: error.message
|
|
1031
|
-
}]);
|
|
1019
|
+
if (error.message.includes("FOREIGN KEY constraint failed")) {
|
|
1020
|
+
const columns = row ? this._probeFkViolation(row) : void 0;
|
|
1021
|
+
throw (0, _atscript_db.constraintViolation)("FK_VIOLATION", columns ? { columns } : {}, error.message);
|
|
1022
|
+
}
|
|
1023
|
+
const unique = error.message.match(/UNIQUE constraint failed:\s*(.+)$/);
|
|
1024
|
+
if (unique) throw (0, _atscript_db.constraintViolation)("CONFLICT", { columns: unique[1].split(",").map((c) => c.trim().replace(/^.*\./, "")).filter(Boolean) }, error.message);
|
|
1032
1025
|
}
|
|
1033
1026
|
throw error;
|
|
1034
1027
|
}
|
|
1035
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* The physical columns of the first foreign key whose (non-null) value in
|
|
1031
|
+
* `row` names no target row — what SQLite's `FOREIGN KEY constraint
|
|
1032
|
+
* failed` leaves out. Runs only on that error path.
|
|
1033
|
+
*/
|
|
1034
|
+
_probeFkViolation(row) {
|
|
1035
|
+
for (const fk of this._table.foreignKeys.values()) {
|
|
1036
|
+
const columns = fk.physicalFields ?? fk.fields;
|
|
1037
|
+
const values = columns.map((c) => row[c]);
|
|
1038
|
+
if (values.some((v) => v === null || v === void 0)) continue;
|
|
1039
|
+
const where = (fk.physicalTargetFields ?? fk.targetFields).map((c) => `"${esc(c)}" = ?`).join(" AND ");
|
|
1040
|
+
const sql = `SELECT 1 AS hit FROM "${esc(fk.targetTable)}" WHERE ${where} LIMIT 1`;
|
|
1041
|
+
try {
|
|
1042
|
+
if (!this.driver.get(sql, values)) return [...columns];
|
|
1043
|
+
} catch {}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1036
1046
|
async insertOne(data) {
|
|
1037
1047
|
const { sql, params } = this._insertSql.build(this.resolveTableName(), data);
|
|
1038
1048
|
this._log(sql, params);
|
|
1039
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1049
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1040
1050
|
return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
|
|
1041
1051
|
}
|
|
1042
1052
|
async insertMany(data) {
|
|
@@ -1054,7 +1064,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1054
1064
|
for (const row of data) {
|
|
1055
1065
|
const { sql, params } = this._insertSql.build(tableName, row);
|
|
1056
1066
|
this._log(sql, params);
|
|
1057
|
-
const result = this._wrapConstraintError(() => this.driver.run(sql, params));
|
|
1067
|
+
const result = this._wrapConstraintError(() => this.driver.run(sql, params), row);
|
|
1058
1068
|
out.push(this._resolveInsertedId(row, result.lastInsertRowid));
|
|
1059
1069
|
}
|
|
1060
1070
|
return out;
|
|
@@ -1082,7 +1092,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1082
1092
|
const sql = `${built.sql} ON CONFLICT DO NOTHING`;
|
|
1083
1093
|
const params = built.params;
|
|
1084
1094
|
this._log(sql, params);
|
|
1085
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1095
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), row));
|
|
1086
1096
|
slots.push(result.changes === 0 ? null : { insertedId: this._resolveInsertedId(row, result.lastInsertRowid) });
|
|
1087
1097
|
}
|
|
1088
1098
|
return slots;
|
|
@@ -1163,7 +1173,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1163
1173
|
params: where.params
|
|
1164
1174
|
}, ops, versionColumn, expectedVersion);
|
|
1165
1175
|
this._log(sql, params);
|
|
1166
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1176
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1167
1177
|
return {
|
|
1168
1178
|
matchedCount: result.changes,
|
|
1169
1179
|
modifiedCount: result.changes
|
|
@@ -1191,7 +1201,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1191
1201
|
const versionColumn = this._versionColumnFor(opts);
|
|
1192
1202
|
const { sql, params } = buildUpdate(this.resolveTableName(), data, where, ops, versionColumn);
|
|
1193
1203
|
this._log(sql, params);
|
|
1194
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1204
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1195
1205
|
return {
|
|
1196
1206
|
matchedCount: result.changes,
|
|
1197
1207
|
modifiedCount: result.changes
|
|
@@ -1201,12 +1211,13 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1201
1211
|
const where = buildWhere(filter);
|
|
1202
1212
|
const tableName = this.resolveTableName();
|
|
1203
1213
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1204
|
-
const
|
|
1214
|
+
const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn, this._table.onUpdateIncPhysical);
|
|
1215
|
+
const { sql, params } = buildUpdate(tableName, full, {
|
|
1205
1216
|
sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
|
|
1206
1217
|
params: where.params
|
|
1207
1218
|
}, this._onUpdateIncOps(), versionColumn, expectedVersion);
|
|
1208
1219
|
this._log(sql, params);
|
|
1209
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1220
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), full));
|
|
1210
1221
|
return {
|
|
1211
1222
|
matchedCount: result.changes,
|
|
1212
1223
|
modifiedCount: result.changes
|
|
@@ -1217,7 +1228,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1217
1228
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1218
1229
|
const { sql, params } = buildUpdate(this.resolveTableName(), data, where, this._onUpdateIncOps(), versionColumn);
|
|
1219
1230
|
this._log(sql, params);
|
|
1220
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1231
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1221
1232
|
return {
|
|
1222
1233
|
matchedCount: result.changes,
|
|
1223
1234
|
modifiedCount: result.changes
|
|
@@ -1582,19 +1593,56 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1582
1593
|
async search(text, query, indexName) {
|
|
1583
1594
|
if (!text.trim()) return [];
|
|
1584
1595
|
const plan = this._searchPlan(text, indexName);
|
|
1585
|
-
|
|
1586
|
-
|
|
1596
|
+
const controls = query.controls || {};
|
|
1597
|
+
const sorted = (0, _atscript_db_sql_tools.orderByList)(sqliteDialect, controls.$sort, controls.$nulls, "t.");
|
|
1598
|
+
if (!plan.join && (sorted || plan.textFields === 0)) {
|
|
1599
|
+
const { sql, params } = buildSelect(this.resolveTableName(), andWhere(plan.predicate, buildWhere(query.filter)), query.controls, (0, _atscript_db_sql_tools.searchRankOrder)(sqliteDialect, {
|
|
1600
|
+
...plan.exact,
|
|
1601
|
+
pkColumns: this._pkColumns()
|
|
1602
|
+
}));
|
|
1587
1603
|
this._log(sql, params);
|
|
1588
1604
|
return this._stmt(() => this.driver.all(sql, params));
|
|
1589
1605
|
}
|
|
1590
|
-
const
|
|
1591
|
-
|
|
1606
|
+
const fts = this._ftsTableName(plan.index.name);
|
|
1607
|
+
let head;
|
|
1608
|
+
let params;
|
|
1609
|
+
let textRank;
|
|
1610
|
+
if (plan.join) {
|
|
1611
|
+
const base = this._buildFtsBase(text, query.filter, plan.index);
|
|
1612
|
+
head = base.fromWhere;
|
|
1613
|
+
params = [...base.params];
|
|
1614
|
+
textRank = {
|
|
1615
|
+
sql: `${this._bm25Expr(`fts."${esc(fts)}"`, plan.index)} ASC`,
|
|
1616
|
+
params: []
|
|
1617
|
+
};
|
|
1618
|
+
} else {
|
|
1619
|
+
const where = buildPrefixedWhere("t", query.filter);
|
|
1620
|
+
const eqs = plan.exact.exactEqualities.map((eq) => `t.${eq}`);
|
|
1621
|
+
head = `FROM "${esc(this.resolveTableName())}" AS t LEFT JOIN m ON m.__atscript_r = t.rowid WHERE (m.__atscript_r IS NOT NULL OR ${eqs.join(" OR ")})` + (where.sql === "1=1" ? "" : ` AND (${where.sql})`);
|
|
1622
|
+
params = [
|
|
1623
|
+
quoteFtsTerm(text),
|
|
1624
|
+
...eqs.map(() => plan.exact.n),
|
|
1625
|
+
...where.params
|
|
1626
|
+
];
|
|
1627
|
+
textRank = {
|
|
1628
|
+
sql: "m.__atscript_s ASC",
|
|
1629
|
+
params: []
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const order = sorted ? {
|
|
1633
|
+
sql: sorted,
|
|
1634
|
+
params: []
|
|
1635
|
+
} : (0, _atscript_db_sql_tools.searchRankOrder)(sqliteDialect, {
|
|
1636
|
+
exactEqualities: plan.join ? [] : plan.exact.exactEqualities.map((eq) => `t.${eq}`),
|
|
1637
|
+
n: plan.exact.n,
|
|
1638
|
+
textRank,
|
|
1639
|
+
pkColumns: this._pkColumns(),
|
|
1640
|
+
prefix: "t."
|
|
1641
|
+
});
|
|
1592
1642
|
let cols = "t.*";
|
|
1593
1643
|
if (controls.$select?.asArray?.length) cols = controls.$select.asArray.map((c) => `t."${esc(c)}"`).join(", ");
|
|
1594
|
-
let sql = `SELECT ${cols} ${
|
|
1595
|
-
|
|
1596
|
-
const orderBy = (0, _atscript_db_sql_tools.orderByList)(sqliteDialect, controls.$sort, controls.$nulls, "t.");
|
|
1597
|
-
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
1644
|
+
let sql = `${plan.join ? "" : `WITH m AS MATERIALIZED (SELECT rowid AS __atscript_r, ${this._bm25Expr(`"${esc(fts)}"`, plan.index)} AS __atscript_s FROM "${esc(fts)}" WHERE "${esc(fts)}" MATCH ?) `}SELECT ${cols} ${head} ORDER BY ${order.sql}`;
|
|
1645
|
+
params.push(...order.params);
|
|
1598
1646
|
if (controls.$limit !== void 0) {
|
|
1599
1647
|
sql += ` LIMIT ?`;
|
|
1600
1648
|
params.push(controls.$limit);
|
|
@@ -1617,7 +1665,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1617
1665
|
let countSql;
|
|
1618
1666
|
let countParams;
|
|
1619
1667
|
if (plan.join) {
|
|
1620
|
-
const base = this._buildFtsBase(text, query.filter,
|
|
1668
|
+
const base = this._buildFtsBase(text, query.filter, plan.index);
|
|
1621
1669
|
countSql = `SELECT COUNT(*) as cnt ${base.fromWhere}`;
|
|
1622
1670
|
countParams = base.params;
|
|
1623
1671
|
} else {
|
|
@@ -1681,6 +1729,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1681
1729
|
const index = this._resolveFtsIndex(indexName);
|
|
1682
1730
|
const { text: textFields, integer: integerFields } = (0, _atscript_db.splitFulltextFields)(index);
|
|
1683
1731
|
const n = integerFields.length > 0 ? (0, _atscript_db.searchTermInteger)(text) : void 0;
|
|
1732
|
+
const equalities = n === void 0 ? [] : integerFields.map((f) => `"${esc(f.name)}" = ?`);
|
|
1684
1733
|
const parts = [];
|
|
1685
1734
|
const params = [];
|
|
1686
1735
|
if (textFields.length > 0) {
|
|
@@ -1688,21 +1737,39 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1688
1737
|
parts.push(`rowid IN (SELECT rowid FROM ${quoted} WHERE ${quoted} MATCH ?)`);
|
|
1689
1738
|
params.push(quoteFtsTerm(text));
|
|
1690
1739
|
}
|
|
1691
|
-
|
|
1692
|
-
parts.push(
|
|
1740
|
+
for (const eq of equalities) {
|
|
1741
|
+
parts.push(eq);
|
|
1693
1742
|
params.push(n);
|
|
1694
1743
|
}
|
|
1695
1744
|
return {
|
|
1745
|
+
index,
|
|
1696
1746
|
predicate: (0, _atscript_db_sql_tools.orFragment)(parts, params),
|
|
1697
|
-
join: textFields.length > 0 && n === void 0
|
|
1747
|
+
join: textFields.length > 0 && n === void 0,
|
|
1748
|
+
textFields: textFields.length,
|
|
1749
|
+
exact: {
|
|
1750
|
+
exactEqualities: equalities,
|
|
1751
|
+
n
|
|
1752
|
+
}
|
|
1698
1753
|
};
|
|
1699
1754
|
}
|
|
1700
1755
|
/**
|
|
1756
|
+
* `bm25(<hidden column>[, w…])` over an index's text members (lower = better). The
|
|
1757
|
+
* per-column weights follow the FTS5 table's column order; omitted when
|
|
1758
|
+
* every member has the default weight.
|
|
1759
|
+
*/
|
|
1760
|
+
_bm25Expr(hidden, index) {
|
|
1761
|
+
const weights = (0, _atscript_db.splitFulltextFields)(index).text.map((f) => f.weight ?? 1);
|
|
1762
|
+
return weights.every((w) => w === 1) ? `bm25(${hidden})` : `bm25(${hidden}, ${weights.map((w) => String(w)).join(", ")})`;
|
|
1763
|
+
}
|
|
1764
|
+
/** The physical primary-key columns — the relevance tie-breaker. */
|
|
1765
|
+
_pkColumns() {
|
|
1766
|
+
return this._table.primaryKeys.map((key) => this._table.physicalPath(key));
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1701
1769
|
* Builds the shared FROM+JOIN+WHERE fragment for FTS5 queries.
|
|
1702
1770
|
* Both data and count queries reuse this to avoid duplicating index resolution and filter translation.
|
|
1703
1771
|
*/
|
|
1704
|
-
_buildFtsBase(text, filter,
|
|
1705
|
-
const ftsIndex = this._resolveFtsIndex(indexName);
|
|
1772
|
+
_buildFtsBase(text, filter, ftsIndex) {
|
|
1706
1773
|
const ftsTable = this._ftsTableName(ftsIndex.name);
|
|
1707
1774
|
const tableName = this.resolveTableName();
|
|
1708
1775
|
const where = buildPrefixedWhere("t", filter);
|
package/dist/index.d.cts
CHANGED
|
@@ -385,6 +385,12 @@ declare class SqliteAdapter extends BaseDbAdapter {
|
|
|
385
385
|
* and rethrow as structured `DbError`.
|
|
386
386
|
*/
|
|
387
387
|
private _wrapConstraintError;
|
|
388
|
+
/**
|
|
389
|
+
* The physical columns of the first foreign key whose (non-null) value in
|
|
390
|
+
* `row` names no target row — what SQLite's `FOREIGN KEY constraint
|
|
391
|
+
* failed` leaves out. Runs only on that error path.
|
|
392
|
+
*/
|
|
393
|
+
private _probeFkViolation;
|
|
388
394
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
389
395
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
390
396
|
supportsInsertIgnore(): boolean;
|
|
@@ -519,6 +525,14 @@ declare class SqliteAdapter extends BaseDbAdapter {
|
|
|
519
525
|
* behavior change for text-only searches).
|
|
520
526
|
*/
|
|
521
527
|
private _searchPlan;
|
|
528
|
+
/**
|
|
529
|
+
* `bm25(<hidden column>[, w…])` over an index's text members (lower = better). The
|
|
530
|
+
* per-column weights follow the FTS5 table's column order; omitted when
|
|
531
|
+
* every member has the default weight.
|
|
532
|
+
*/
|
|
533
|
+
private _bm25Expr;
|
|
534
|
+
/** The physical primary-key columns — the relevance tie-breaker. */
|
|
535
|
+
private _pkColumns;
|
|
522
536
|
/**
|
|
523
537
|
* Builds the shared FROM+JOIN+WHERE fragment for FTS5 queries.
|
|
524
538
|
* Both data and count queries reuse this to avoid duplicating index resolution and filter translation.
|
package/dist/index.d.mts
CHANGED
|
@@ -385,6 +385,12 @@ declare class SqliteAdapter extends BaseDbAdapter {
|
|
|
385
385
|
* and rethrow as structured `DbError`.
|
|
386
386
|
*/
|
|
387
387
|
private _wrapConstraintError;
|
|
388
|
+
/**
|
|
389
|
+
* The physical columns of the first foreign key whose (non-null) value in
|
|
390
|
+
* `row` names no target row — what SQLite's `FOREIGN KEY constraint
|
|
391
|
+
* failed` leaves out. Runs only on that error path.
|
|
392
|
+
*/
|
|
393
|
+
private _probeFkViolation;
|
|
388
394
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
389
395
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
390
396
|
supportsInsertIgnore(): boolean;
|
|
@@ -519,6 +525,14 @@ declare class SqliteAdapter extends BaseDbAdapter {
|
|
|
519
525
|
* behavior change for text-only searches).
|
|
520
526
|
*/
|
|
521
527
|
private _searchPlan;
|
|
528
|
+
/**
|
|
529
|
+
* `bm25(<hidden column>[, w…])` over an index's text members (lower = better). The
|
|
530
|
+
* per-column weights follow the FTS5 table's column order; omitted when
|
|
531
|
+
* every member has the default weight.
|
|
532
|
+
*/
|
|
533
|
+
private _bm25Expr;
|
|
534
|
+
/** The physical primary-key columns — the relevance tie-breaker. */
|
|
535
|
+
private _pkColumns;
|
|
522
536
|
/**
|
|
523
537
|
* Builds the shared FROM+JOIN+WHERE fragment for FTS5 queries.
|
|
524
538
|
* Both data and count queries reuse this to avoid duplicating index resolution and filter translation.
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, createChangeRecorder, defaultFulltextIndex, describeFulltext, fkColumns, searchIndexNotFoundMessage, searchTermInteger, splitFulltextFields, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, constraintViolation, createChangeRecorder, defaultFulltextIndex, describeFulltext, fkColumns, searchIndexNotFoundMessage, searchTermInteger, splitFulltextFields, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
3
3
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
4
|
-
import { EMPTY_AND, InsertSqlCache, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, fromSourceSql, geoWindowFromControls, jsonDollarPath, orFragment, orderByList, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, viewReadSource, writeLimitWhere } from "@atscript/db-sql-tools";
|
|
4
|
+
import { EMPTY_AND, InsertSqlCache, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, fromSourceSql, geoWindowFromControls, jsonDollarPath, orFragment, orderByList, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, searchRankOrder, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, viewReadSource, writeLimitWhere } from "@atscript/db-sql-tools";
|
|
5
5
|
import { bucketer } from "@uniqu/core";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
//#region src/better-sqlite3-driver.ts
|
|
@@ -378,8 +378,8 @@ function haversineDistanceExpr(quotedCol, point) {
|
|
|
378
378
|
/**
|
|
379
379
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
380
380
|
*/
|
|
381
|
-
function buildSelect$1(table, where, controls) {
|
|
382
|
-
return buildSelect(sqliteDialect, table, where, controls);
|
|
381
|
+
function buildSelect$1(table, where, controls, defaultOrder) {
|
|
382
|
+
return buildSelect(sqliteDialect, table, where, controls, void 0, defaultOrder);
|
|
383
383
|
}
|
|
384
384
|
/**
|
|
385
385
|
* Builds an UPDATE ... SET ... WHERE statement.
|
|
@@ -1010,32 +1010,42 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1010
1010
|
* Wraps a write operation to catch native SQLite constraint errors
|
|
1011
1011
|
* and rethrow as structured `DbError`.
|
|
1012
1012
|
*/
|
|
1013
|
-
_wrapConstraintError(fn) {
|
|
1013
|
+
_wrapConstraintError(fn, row) {
|
|
1014
1014
|
try {
|
|
1015
1015
|
return fn();
|
|
1016
1016
|
} catch (error) {
|
|
1017
1017
|
if (error instanceof Error) {
|
|
1018
|
-
if (error.message.includes("FOREIGN KEY constraint failed"))
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
const
|
|
1023
|
-
if (
|
|
1024
|
-
path: uniqueMatch[1],
|
|
1025
|
-
message: error.message
|
|
1026
|
-
}]);
|
|
1027
|
-
if (error.message.includes("UNIQUE constraint failed")) throw new DbError("CONFLICT", [{
|
|
1028
|
-
path: "",
|
|
1029
|
-
message: error.message
|
|
1030
|
-
}]);
|
|
1018
|
+
if (error.message.includes("FOREIGN KEY constraint failed")) {
|
|
1019
|
+
const columns = row ? this._probeFkViolation(row) : void 0;
|
|
1020
|
+
throw constraintViolation("FK_VIOLATION", columns ? { columns } : {}, error.message);
|
|
1021
|
+
}
|
|
1022
|
+
const unique = error.message.match(/UNIQUE constraint failed:\s*(.+)$/);
|
|
1023
|
+
if (unique) throw constraintViolation("CONFLICT", { columns: unique[1].split(",").map((c) => c.trim().replace(/^.*\./, "")).filter(Boolean) }, error.message);
|
|
1031
1024
|
}
|
|
1032
1025
|
throw error;
|
|
1033
1026
|
}
|
|
1034
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The physical columns of the first foreign key whose (non-null) value in
|
|
1030
|
+
* `row` names no target row — what SQLite's `FOREIGN KEY constraint
|
|
1031
|
+
* failed` leaves out. Runs only on that error path.
|
|
1032
|
+
*/
|
|
1033
|
+
_probeFkViolation(row) {
|
|
1034
|
+
for (const fk of this._table.foreignKeys.values()) {
|
|
1035
|
+
const columns = fk.physicalFields ?? fk.fields;
|
|
1036
|
+
const values = columns.map((c) => row[c]);
|
|
1037
|
+
if (values.some((v) => v === null || v === void 0)) continue;
|
|
1038
|
+
const where = (fk.physicalTargetFields ?? fk.targetFields).map((c) => `"${esc(c)}" = ?`).join(" AND ");
|
|
1039
|
+
const sql = `SELECT 1 AS hit FROM "${esc(fk.targetTable)}" WHERE ${where} LIMIT 1`;
|
|
1040
|
+
try {
|
|
1041
|
+
if (!this.driver.get(sql, values)) return [...columns];
|
|
1042
|
+
} catch {}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1035
1045
|
async insertOne(data) {
|
|
1036
1046
|
const { sql, params } = this._insertSql.build(this.resolveTableName(), data);
|
|
1037
1047
|
this._log(sql, params);
|
|
1038
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1048
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1039
1049
|
return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
|
|
1040
1050
|
}
|
|
1041
1051
|
async insertMany(data) {
|
|
@@ -1053,7 +1063,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1053
1063
|
for (const row of data) {
|
|
1054
1064
|
const { sql, params } = this._insertSql.build(tableName, row);
|
|
1055
1065
|
this._log(sql, params);
|
|
1056
|
-
const result = this._wrapConstraintError(() => this.driver.run(sql, params));
|
|
1066
|
+
const result = this._wrapConstraintError(() => this.driver.run(sql, params), row);
|
|
1057
1067
|
out.push(this._resolveInsertedId(row, result.lastInsertRowid));
|
|
1058
1068
|
}
|
|
1059
1069
|
return out;
|
|
@@ -1081,7 +1091,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1081
1091
|
const sql = `${built.sql} ON CONFLICT DO NOTHING`;
|
|
1082
1092
|
const params = built.params;
|
|
1083
1093
|
this._log(sql, params);
|
|
1084
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1094
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), row));
|
|
1085
1095
|
slots.push(result.changes === 0 ? null : { insertedId: this._resolveInsertedId(row, result.lastInsertRowid) });
|
|
1086
1096
|
}
|
|
1087
1097
|
return slots;
|
|
@@ -1162,7 +1172,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1162
1172
|
params: where.params
|
|
1163
1173
|
}, ops, versionColumn, expectedVersion);
|
|
1164
1174
|
this._log(sql, params);
|
|
1165
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1175
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1166
1176
|
return {
|
|
1167
1177
|
matchedCount: result.changes,
|
|
1168
1178
|
modifiedCount: result.changes
|
|
@@ -1190,7 +1200,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1190
1200
|
const versionColumn = this._versionColumnFor(opts);
|
|
1191
1201
|
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, ops, versionColumn);
|
|
1192
1202
|
this._log(sql, params);
|
|
1193
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1203
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1194
1204
|
return {
|
|
1195
1205
|
matchedCount: result.changes,
|
|
1196
1206
|
modifiedCount: result.changes
|
|
@@ -1200,12 +1210,13 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1200
1210
|
const where = buildWhere(filter);
|
|
1201
1211
|
const tableName = this.resolveTableName();
|
|
1202
1212
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1203
|
-
const
|
|
1213
|
+
const full = fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn, this._table.onUpdateIncPhysical);
|
|
1214
|
+
const { sql, params } = buildUpdate$1(tableName, full, {
|
|
1204
1215
|
sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
|
|
1205
1216
|
params: where.params
|
|
1206
1217
|
}, this._onUpdateIncOps(), versionColumn, expectedVersion);
|
|
1207
1218
|
this._log(sql, params);
|
|
1208
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1219
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), full));
|
|
1209
1220
|
return {
|
|
1210
1221
|
matchedCount: result.changes,
|
|
1211
1222
|
modifiedCount: result.changes
|
|
@@ -1216,7 +1227,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1216
1227
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1217
1228
|
const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, this._onUpdateIncOps(), versionColumn);
|
|
1218
1229
|
this._log(sql, params);
|
|
1219
|
-
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
|
|
1230
|
+
const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params), data));
|
|
1220
1231
|
return {
|
|
1221
1232
|
matchedCount: result.changes,
|
|
1222
1233
|
modifiedCount: result.changes
|
|
@@ -1581,19 +1592,56 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1581
1592
|
async search(text, query, indexName) {
|
|
1582
1593
|
if (!text.trim()) return [];
|
|
1583
1594
|
const plan = this._searchPlan(text, indexName);
|
|
1584
|
-
|
|
1585
|
-
|
|
1595
|
+
const controls = query.controls || {};
|
|
1596
|
+
const sorted = orderByList(sqliteDialect, controls.$sort, controls.$nulls, "t.");
|
|
1597
|
+
if (!plan.join && (sorted || plan.textFields === 0)) {
|
|
1598
|
+
const { sql, params } = buildSelect$1(this.resolveTableName(), andWhere(plan.predicate, buildWhere(query.filter)), query.controls, searchRankOrder(sqliteDialect, {
|
|
1599
|
+
...plan.exact,
|
|
1600
|
+
pkColumns: this._pkColumns()
|
|
1601
|
+
}));
|
|
1586
1602
|
this._log(sql, params);
|
|
1587
1603
|
return this._stmt(() => this.driver.all(sql, params));
|
|
1588
1604
|
}
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1605
|
+
const fts = this._ftsTableName(plan.index.name);
|
|
1606
|
+
let head;
|
|
1607
|
+
let params;
|
|
1608
|
+
let textRank;
|
|
1609
|
+
if (plan.join) {
|
|
1610
|
+
const base = this._buildFtsBase(text, query.filter, plan.index);
|
|
1611
|
+
head = base.fromWhere;
|
|
1612
|
+
params = [...base.params];
|
|
1613
|
+
textRank = {
|
|
1614
|
+
sql: `${this._bm25Expr(`fts."${esc(fts)}"`, plan.index)} ASC`,
|
|
1615
|
+
params: []
|
|
1616
|
+
};
|
|
1617
|
+
} else {
|
|
1618
|
+
const where = buildPrefixedWhere("t", query.filter);
|
|
1619
|
+
const eqs = plan.exact.exactEqualities.map((eq) => `t.${eq}`);
|
|
1620
|
+
head = `FROM "${esc(this.resolveTableName())}" AS t LEFT JOIN m ON m.__atscript_r = t.rowid WHERE (m.__atscript_r IS NOT NULL OR ${eqs.join(" OR ")})` + (where.sql === "1=1" ? "" : ` AND (${where.sql})`);
|
|
1621
|
+
params = [
|
|
1622
|
+
quoteFtsTerm(text),
|
|
1623
|
+
...eqs.map(() => plan.exact.n),
|
|
1624
|
+
...where.params
|
|
1625
|
+
];
|
|
1626
|
+
textRank = {
|
|
1627
|
+
sql: "m.__atscript_s ASC",
|
|
1628
|
+
params: []
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const order = sorted ? {
|
|
1632
|
+
sql: sorted,
|
|
1633
|
+
params: []
|
|
1634
|
+
} : searchRankOrder(sqliteDialect, {
|
|
1635
|
+
exactEqualities: plan.join ? [] : plan.exact.exactEqualities.map((eq) => `t.${eq}`),
|
|
1636
|
+
n: plan.exact.n,
|
|
1637
|
+
textRank,
|
|
1638
|
+
pkColumns: this._pkColumns(),
|
|
1639
|
+
prefix: "t."
|
|
1640
|
+
});
|
|
1591
1641
|
let cols = "t.*";
|
|
1592
1642
|
if (controls.$select?.asArray?.length) cols = controls.$select.asArray.map((c) => `t."${esc(c)}"`).join(", ");
|
|
1593
|
-
let sql = `SELECT ${cols} ${
|
|
1594
|
-
|
|
1595
|
-
const orderBy = orderByList(sqliteDialect, controls.$sort, controls.$nulls, "t.");
|
|
1596
|
-
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
1643
|
+
let sql = `${plan.join ? "" : `WITH m AS MATERIALIZED (SELECT rowid AS __atscript_r, ${this._bm25Expr(`"${esc(fts)}"`, plan.index)} AS __atscript_s FROM "${esc(fts)}" WHERE "${esc(fts)}" MATCH ?) `}SELECT ${cols} ${head} ORDER BY ${order.sql}`;
|
|
1644
|
+
params.push(...order.params);
|
|
1597
1645
|
if (controls.$limit !== void 0) {
|
|
1598
1646
|
sql += ` LIMIT ?`;
|
|
1599
1647
|
params.push(controls.$limit);
|
|
@@ -1616,7 +1664,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1616
1664
|
let countSql;
|
|
1617
1665
|
let countParams;
|
|
1618
1666
|
if (plan.join) {
|
|
1619
|
-
const base = this._buildFtsBase(text, query.filter,
|
|
1667
|
+
const base = this._buildFtsBase(text, query.filter, plan.index);
|
|
1620
1668
|
countSql = `SELECT COUNT(*) as cnt ${base.fromWhere}`;
|
|
1621
1669
|
countParams = base.params;
|
|
1622
1670
|
} else {
|
|
@@ -1680,6 +1728,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1680
1728
|
const index = this._resolveFtsIndex(indexName);
|
|
1681
1729
|
const { text: textFields, integer: integerFields } = splitFulltextFields(index);
|
|
1682
1730
|
const n = integerFields.length > 0 ? searchTermInteger(text) : void 0;
|
|
1731
|
+
const equalities = n === void 0 ? [] : integerFields.map((f) => `"${esc(f.name)}" = ?`);
|
|
1683
1732
|
const parts = [];
|
|
1684
1733
|
const params = [];
|
|
1685
1734
|
if (textFields.length > 0) {
|
|
@@ -1687,21 +1736,39 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
|
|
|
1687
1736
|
parts.push(`rowid IN (SELECT rowid FROM ${quoted} WHERE ${quoted} MATCH ?)`);
|
|
1688
1737
|
params.push(quoteFtsTerm(text));
|
|
1689
1738
|
}
|
|
1690
|
-
|
|
1691
|
-
parts.push(
|
|
1739
|
+
for (const eq of equalities) {
|
|
1740
|
+
parts.push(eq);
|
|
1692
1741
|
params.push(n);
|
|
1693
1742
|
}
|
|
1694
1743
|
return {
|
|
1744
|
+
index,
|
|
1695
1745
|
predicate: orFragment(parts, params),
|
|
1696
|
-
join: textFields.length > 0 && n === void 0
|
|
1746
|
+
join: textFields.length > 0 && n === void 0,
|
|
1747
|
+
textFields: textFields.length,
|
|
1748
|
+
exact: {
|
|
1749
|
+
exactEqualities: equalities,
|
|
1750
|
+
n
|
|
1751
|
+
}
|
|
1697
1752
|
};
|
|
1698
1753
|
}
|
|
1699
1754
|
/**
|
|
1755
|
+
* `bm25(<hidden column>[, w…])` over an index's text members (lower = better). The
|
|
1756
|
+
* per-column weights follow the FTS5 table's column order; omitted when
|
|
1757
|
+
* every member has the default weight.
|
|
1758
|
+
*/
|
|
1759
|
+
_bm25Expr(hidden, index) {
|
|
1760
|
+
const weights = splitFulltextFields(index).text.map((f) => f.weight ?? 1);
|
|
1761
|
+
return weights.every((w) => w === 1) ? `bm25(${hidden})` : `bm25(${hidden}, ${weights.map((w) => String(w)).join(", ")})`;
|
|
1762
|
+
}
|
|
1763
|
+
/** The physical primary-key columns — the relevance tie-breaker. */
|
|
1764
|
+
_pkColumns() {
|
|
1765
|
+
return this._table.primaryKeys.map((key) => this._table.physicalPath(key));
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1700
1768
|
* Builds the shared FROM+JOIN+WHERE fragment for FTS5 queries.
|
|
1701
1769
|
* Both data and count queries reuse this to avoid duplicating index resolution and filter translation.
|
|
1702
1770
|
*/
|
|
1703
|
-
_buildFtsBase(text, filter,
|
|
1704
|
-
const ftsIndex = this._resolveFtsIndex(indexName);
|
|
1771
|
+
_buildFtsBase(text, filter, ftsIndex) {
|
|
1705
1772
|
const ftsTable = this._ftsTableName(ftsIndex.name);
|
|
1706
1773
|
const tableName = this.resolveTableName();
|
|
1707
1774
|
const where = buildPrefixedWhere("t", filter);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-sqlite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.165",
|
|
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
|
|
40
|
-
"@atscript/typescript": "^0.1
|
|
39
|
+
"@atscript/core": "^0.2.1",
|
|
40
|
+
"@atscript/typescript": "^0.2.1",
|
|
41
41
|
"@types/better-sqlite3": "^7.6.13",
|
|
42
42
|
"@uniqu/core": "^0.1.15",
|
|
43
43
|
"better-sqlite3": "^12.6.2",
|
|
44
44
|
"sqlite-vec": "^0.1.9",
|
|
45
|
-
"unplugin-atscript": "^0.1
|
|
45
|
+
"unplugin-atscript": "^0.2.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@atscript/core": "^0.1
|
|
49
|
-
"@atscript/typescript": "^0.1
|
|
48
|
+
"@atscript/core": "^0.2.1",
|
|
49
|
+
"@atscript/typescript": "^0.2.1",
|
|
50
50
|
"@uniqu/core": "^0.1.15",
|
|
51
51
|
"better-sqlite3": ">=11.0.0",
|
|
52
|
-
"@atscript/db": "^0.1.
|
|
53
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
52
|
+
"@atscript/db": "^0.1.165",
|
|
53
|
+
"@atscript/db-sql-tools": "^0.1.165"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"better-sqlite3": {
|