@tanstack/db-sqlite-persistence-core 0.4.1 → 0.4.4
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/cjs/errors.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +47 -52
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.cjs +303 -162
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/persisted.js +49 -54
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.js +304 -163
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +1 -2
- package/src/persisted.ts +80 -75
- package/src/remote-subset-wire.ts +1 -6
- package/src/sqlite-core-adapter.ts +478 -214
|
@@ -36,7 +36,7 @@ type SqliteSupportedValue = null | number | string
|
|
|
36
36
|
|
|
37
37
|
// The default stays below SQLite's older 999-variable limit. Drivers with a
|
|
38
38
|
// lower binding cap use smaller chunks; each replacement row binds four values.
|
|
39
|
-
const REPLACEMENT_BATCH_SIZE =
|
|
39
|
+
const REPLACEMENT_BATCH_SIZE = 125
|
|
40
40
|
|
|
41
41
|
type CollectionTableMapping = {
|
|
42
42
|
tableName: string
|
|
@@ -60,9 +60,7 @@ type StoredSqliteRow = {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
type SQLiteCoreAdapterSchemaMismatchPolicy =
|
|
63
|
-
| `sync-
|
|
64
|
-
| `sync-absent-error`
|
|
65
|
-
| `reset`
|
|
63
|
+
`sync-present-reset` | `sync-absent-error` | `reset`
|
|
66
64
|
|
|
67
65
|
export type SQLiteCoreAdapterOptions = {
|
|
68
66
|
driver: SQLiteDriver
|
|
@@ -222,6 +220,7 @@ function observeSharedLogicalSchedulingSupport(
|
|
|
222
220
|
}
|
|
223
221
|
|
|
224
222
|
return {
|
|
223
|
+
maxBoundParameters: driver.maxBoundParameters,
|
|
225
224
|
exec: (sql) => observe(driver.exec(sql)),
|
|
226
225
|
query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>
|
|
227
226
|
observe(driver.query<T>(sql, params)),
|
|
@@ -256,16 +255,11 @@ export const DEFAULT_APPLIED_TX_PRUNE_MAX_ROWS = 1_000
|
|
|
256
255
|
*/
|
|
257
256
|
export const DEFAULT_APPLIED_TX_PRUNE_MAX_AGE_SECONDS = 24 * 60 * 60
|
|
258
257
|
|
|
259
|
-
const SQLITE_MAX_IN_BATCH_SIZE = 900
|
|
260
258
|
const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
261
259
|
const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/
|
|
262
260
|
type CompiledValueKind = `unknown` | `bigint` | `date` | `datetime`
|
|
263
261
|
type PersistedTaggedValueType =
|
|
264
|
-
| `
|
|
265
|
-
| `date`
|
|
266
|
-
| `nan`
|
|
267
|
-
| `infinity`
|
|
268
|
-
| `-infinity`
|
|
262
|
+
`bigint` | `date` | `nan` | `infinity` | `-infinity`
|
|
269
263
|
type PersistedTaggedValue = {
|
|
270
264
|
[PERSISTED_TYPE_TAG]: PersistedTaggedValueType
|
|
271
265
|
[PERSISTED_VALUE_TAG]: string
|
|
@@ -610,6 +604,39 @@ function createJsonPath(path: Array<string>): string | null {
|
|
|
610
604
|
return jsonPath
|
|
611
605
|
}
|
|
612
606
|
|
|
607
|
+
const MAX_INDEXED_NUMERIC_PATH_SEGMENTS = 3
|
|
608
|
+
|
|
609
|
+
function hasDeepNumericPath(expression: IR.BasicExpression): boolean {
|
|
610
|
+
if (expression.type === `ref`) {
|
|
611
|
+
return (
|
|
612
|
+
IR.getPropRefPropertyPath(expression).filter((segment) =>
|
|
613
|
+
/^[0-9]+$/.test(String(segment)),
|
|
614
|
+
).length > MAX_INDEXED_NUMERIC_PATH_SEGMENTS
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
return expression.type === `func` && expression.args.some(hasDeepNumericPath)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function createJsonPathVariants(path: Array<string>): Array<string> | null {
|
|
621
|
+
const canonical = createJsonPath(path)
|
|
622
|
+
if (!canonical) return null
|
|
623
|
+
|
|
624
|
+
let paths = [`$`]
|
|
625
|
+
for (const segment of path) {
|
|
626
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)) {
|
|
627
|
+
paths = paths.map((prefix) => `${prefix}.${segment}`)
|
|
628
|
+
} else if (/^[0-9]+$/.test(segment)) {
|
|
629
|
+
if (paths.length >= 2 ** MAX_INDEXED_NUMERIC_PATH_SEGMENTS)
|
|
630
|
+
return [canonical]
|
|
631
|
+
paths = paths.flatMap((prefix) => [
|
|
632
|
+
`${prefix}[${segment}]`,
|
|
633
|
+
`${prefix}."${segment}"`,
|
|
634
|
+
])
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return paths
|
|
638
|
+
}
|
|
639
|
+
|
|
613
640
|
function getLiteralValueKind(value: unknown): CompiledValueKind {
|
|
614
641
|
if (typeof value === `bigint`) {
|
|
615
642
|
return `bigint`
|
|
@@ -788,11 +815,54 @@ function argumentCompilationContext(
|
|
|
788
815
|
}
|
|
789
816
|
}
|
|
790
817
|
|
|
818
|
+
function hasUnpairedSurrogate(value: string): boolean {
|
|
819
|
+
// With /u, paired surrogates form one code point; lone surrogates still match.
|
|
820
|
+
return /[\uD800-\uDFFF]/u.test(value)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function isSafeEqualityLiteral(value: unknown): boolean {
|
|
824
|
+
return (
|
|
825
|
+
typeof value === `string` ||
|
|
826
|
+
typeof value === `boolean` ||
|
|
827
|
+
typeof value === `bigint`
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function isSafeCoalesceOperand(
|
|
832
|
+
expression: IR.BasicExpression | undefined,
|
|
833
|
+
): boolean {
|
|
834
|
+
return (
|
|
835
|
+
expression?.type === `func` &&
|
|
836
|
+
expression.name === `coalesce` &&
|
|
837
|
+
expression.args.length === 2 &&
|
|
838
|
+
expression.args[0]?.type === `ref` &&
|
|
839
|
+
expression.args[1]?.type === `val` &&
|
|
840
|
+
isSafeEqualityLiteral(expression.args[1].value) &&
|
|
841
|
+
!(
|
|
842
|
+
typeof expression.args[1].value === `string` &&
|
|
843
|
+
hasUnpairedSurrogate(expression.args[1].value)
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
}
|
|
847
|
+
|
|
791
848
|
function compileSqlExpression(
|
|
792
849
|
expression: IR.BasicExpression,
|
|
793
850
|
context: SqlExpressionCompilationContext = `predicate`,
|
|
794
851
|
): CompiledSqlFragment {
|
|
795
852
|
if (expression.type === `val`) {
|
|
853
|
+
if (
|
|
854
|
+
context === `predicate` &&
|
|
855
|
+
typeof expression.value === `string` &&
|
|
856
|
+
hasUnpairedSurrogate(expression.value)
|
|
857
|
+
) {
|
|
858
|
+
// JSON extraction changes lone surrogates in stored strings. Apply the
|
|
859
|
+
// same extraction to the binding so an indexed equality keeps its row.
|
|
860
|
+
return {
|
|
861
|
+
supported: true,
|
|
862
|
+
sql: `json_extract(?, '$')`,
|
|
863
|
+
params: [JSON.stringify(expression.value)],
|
|
864
|
+
}
|
|
865
|
+
}
|
|
796
866
|
const valueKind = getLiteralValueKind(expression.value)
|
|
797
867
|
return {
|
|
798
868
|
supported: true,
|
|
@@ -809,10 +879,10 @@ function compileSqlExpression(
|
|
|
809
879
|
}
|
|
810
880
|
|
|
811
881
|
if (expression.type === `ref`) {
|
|
812
|
-
const
|
|
882
|
+
const jsonPaths = createJsonPathVariants(
|
|
813
883
|
IR.getPropRefPropertyPath(expression).map(String),
|
|
814
884
|
)
|
|
815
|
-
if (!
|
|
885
|
+
if (!jsonPaths) {
|
|
816
886
|
return {
|
|
817
887
|
supported: false,
|
|
818
888
|
sql: ``,
|
|
@@ -820,7 +890,13 @@ function compileSqlExpression(
|
|
|
820
890
|
}
|
|
821
891
|
}
|
|
822
892
|
|
|
823
|
-
return compileRefExpressionSql(
|
|
893
|
+
if (jsonPaths.length === 1) return compileRefExpressionSql(jsonPaths[0]!)
|
|
894
|
+
return {
|
|
895
|
+
supported: true,
|
|
896
|
+
sql: `COALESCE(${jsonPaths.map((path) => compileRefExpressionSql(path).sql).join(`, `)})`,
|
|
897
|
+
params: [],
|
|
898
|
+
valueKind: `unknown`,
|
|
899
|
+
}
|
|
824
900
|
}
|
|
825
901
|
|
|
826
902
|
const compiledArgs = expression.args.map((arg, index) =>
|
|
@@ -888,23 +964,20 @@ function compileSqlExpression(
|
|
|
888
964
|
params,
|
|
889
965
|
}
|
|
890
966
|
}
|
|
891
|
-
case `and`:
|
|
892
|
-
if (argSql.length < 2) {
|
|
893
|
-
return { supported: false, sql: ``, params: [] }
|
|
894
|
-
}
|
|
895
|
-
return {
|
|
896
|
-
supported: true,
|
|
897
|
-
sql: argSql.map((sql) => `(${sql})`).join(` AND `),
|
|
898
|
-
params,
|
|
899
|
-
}
|
|
900
|
-
}
|
|
967
|
+
case `and`:
|
|
901
968
|
case `or`: {
|
|
902
|
-
if (argSql.length
|
|
903
|
-
return {
|
|
969
|
+
if (argSql.length === 0) {
|
|
970
|
+
return {
|
|
971
|
+
supported: true,
|
|
972
|
+
sql: expression.name === `and` ? `(1 = 1)` : `(0 = 1)`,
|
|
973
|
+
params: [],
|
|
974
|
+
}
|
|
904
975
|
}
|
|
905
976
|
return {
|
|
906
977
|
supported: true,
|
|
907
|
-
sql: argSql
|
|
978
|
+
sql: argSql
|
|
979
|
+
.map((sql) => `(${sql})`)
|
|
980
|
+
.join(expression.name === `and` ? ` AND ` : ` OR `),
|
|
908
981
|
params,
|
|
909
982
|
}
|
|
910
983
|
}
|
|
@@ -948,44 +1021,17 @@ function compileSqlExpression(
|
|
|
948
1021
|
}
|
|
949
1022
|
}
|
|
950
1023
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
startIndex += SQLITE_MAX_IN_BATCH_SIZE
|
|
959
|
-
) {
|
|
960
|
-
const chunkValues = listValue.slice(
|
|
961
|
-
startIndex,
|
|
962
|
-
startIndex + SQLITE_MAX_IN_BATCH_SIZE,
|
|
963
|
-
)
|
|
964
|
-
const chunkParams: Array<SqliteSupportedValue> = []
|
|
965
|
-
const chunkValueSql = chunkValues.map((value) => {
|
|
966
|
-
chunkParams.push(toSqliteParameterValue(value))
|
|
967
|
-
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`
|
|
968
|
-
})
|
|
969
|
-
chunkClauses.push(`(${leftSql} IN (${chunkValueSql.join(`, `)}))`)
|
|
970
|
-
batchedParams.push(...leftParams, ...chunkParams)
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
return {
|
|
974
|
-
supported: true,
|
|
975
|
-
sql: `(${chunkClauses.join(` OR `)})`,
|
|
976
|
-
params: batchedParams,
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const listParams: Array<SqliteSupportedValue> = []
|
|
981
|
-
const listValueSql = listValue.map((value) => {
|
|
982
|
-
listParams.push(toSqliteParameterValue(value))
|
|
983
|
-
return typeof value === `bigint` ? `CAST(? AS NUMERIC)` : `?`
|
|
984
|
-
})
|
|
1024
|
+
const jsonList = `[${listValue
|
|
1025
|
+
.map((value) =>
|
|
1026
|
+
typeof value === `bigint`
|
|
1027
|
+
? assertSQLiteBigIntInRange(value).toString()
|
|
1028
|
+
: JSON.stringify(toSqliteParameterValue(value)),
|
|
1029
|
+
)
|
|
1030
|
+
.join(`,`)}]`
|
|
985
1031
|
return {
|
|
986
1032
|
supported: true,
|
|
987
|
-
sql: `(${leftSql} IN (
|
|
988
|
-
params: [...leftParams,
|
|
1033
|
+
sql: `(${leftSql} IN (SELECT value FROM json_each(?)))`,
|
|
1034
|
+
params: [...leftParams, jsonList],
|
|
989
1035
|
}
|
|
990
1036
|
}
|
|
991
1037
|
case `like`:
|
|
@@ -1050,6 +1096,160 @@ function compileSqlExpression(
|
|
|
1050
1096
|
}
|
|
1051
1097
|
}
|
|
1052
1098
|
|
|
1099
|
+
function compileSafeSqlPrefilter(
|
|
1100
|
+
expression: IR.BasicExpression,
|
|
1101
|
+
compiled?: CompiledSqlFragment,
|
|
1102
|
+
): CompiledSqlFragment | undefined {
|
|
1103
|
+
// Every public match must pass this SQL candidate before JavaScript applies
|
|
1104
|
+
// the authoritative row predicate. Extra candidates are allowed.
|
|
1105
|
+
const direct = () => {
|
|
1106
|
+
const candidate = compiled ?? compileSqlExpression(expression)
|
|
1107
|
+
return candidate.supported ? candidate : undefined
|
|
1108
|
+
}
|
|
1109
|
+
if (expression.type === `val`) {
|
|
1110
|
+
return typeof expression.value === `boolean` || expression.value == null
|
|
1111
|
+
? direct()
|
|
1112
|
+
: undefined
|
|
1113
|
+
}
|
|
1114
|
+
if (expression.type === `ref`) return undefined
|
|
1115
|
+
|
|
1116
|
+
if (expression.name === `and` || expression.name === `or`) {
|
|
1117
|
+
if (expression.args.length === 0) return direct()
|
|
1118
|
+
const candidates = expression.args.map((argument) =>
|
|
1119
|
+
compileSafeSqlPrefilter(argument),
|
|
1120
|
+
)
|
|
1121
|
+
if (expression.name === `or` && candidates.some((candidate) => !candidate))
|
|
1122
|
+
return undefined
|
|
1123
|
+
const selected = candidates.filter(
|
|
1124
|
+
(candidate): candidate is CompiledSqlFragment => candidate !== undefined,
|
|
1125
|
+
)
|
|
1126
|
+
if (selected.length === 0) return undefined
|
|
1127
|
+
return {
|
|
1128
|
+
supported: true,
|
|
1129
|
+
sql: selected
|
|
1130
|
+
.map(({ sql }) => `(${sql})`)
|
|
1131
|
+
.join(expression.name === `and` ? ` AND ` : ` OR `),
|
|
1132
|
+
params: selected.flatMap(({ params }) => params),
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// More than three digit segments need too many alternative JSON paths.
|
|
1137
|
+
// Keep their prefilter unbounded rather than exclude an object-key match.
|
|
1138
|
+
if (hasDeepNumericPath(expression)) return undefined
|
|
1139
|
+
compiled ??= compileSqlExpression(expression)
|
|
1140
|
+
if (!compiled.supported) return undefined
|
|
1141
|
+
const [left, right] = expression.args
|
|
1142
|
+
if (expression.args.length === 2) {
|
|
1143
|
+
if (
|
|
1144
|
+
[`gt`, `gte`, `lt`, `lte`].includes(expression.name) &&
|
|
1145
|
+
(left?.type === `ref` || right?.type === `ref`)
|
|
1146
|
+
) {
|
|
1147
|
+
const field =
|
|
1148
|
+
left?.type === `ref` ? left : right?.type === `ref` ? right : undefined
|
|
1149
|
+
const literal = field === left ? right : left
|
|
1150
|
+
if (
|
|
1151
|
+
field &&
|
|
1152
|
+
literal?.type === `val` &&
|
|
1153
|
+
((typeof literal.value === `number` &&
|
|
1154
|
+
Number.isSafeInteger(literal.value)) ||
|
|
1155
|
+
typeof literal.value === `bigint`)
|
|
1156
|
+
) {
|
|
1157
|
+
const fieldSql = compileSqlExpression(field, `index-expression`).sql
|
|
1158
|
+
const literalSql = compileSqlExpression(
|
|
1159
|
+
literal,
|
|
1160
|
+
typeof literal.value === `bigint` ? `index-expression` : `predicate`,
|
|
1161
|
+
)
|
|
1162
|
+
const greaterSide =
|
|
1163
|
+
(field === left && [`gt`, `gte`].includes(expression.name)) ||
|
|
1164
|
+
(field === right && [`lt`, `lte`].includes(expression.name))
|
|
1165
|
+
const unsafeBigInt =
|
|
1166
|
+
typeof literal.value === `bigint` &&
|
|
1167
|
+
!Number.isSafeInteger(Number(literal.value))
|
|
1168
|
+
const roundedNumberCandidates = unsafeBigInt
|
|
1169
|
+
? literal.value > 0n
|
|
1170
|
+
? ` OR ${fieldSql} >= ${Number.MAX_SAFE_INTEGER}`
|
|
1171
|
+
: ` OR ${fieldSql} <= -${Number.MAX_SAFE_INTEGER}`
|
|
1172
|
+
: ``
|
|
1173
|
+
return {
|
|
1174
|
+
supported: true,
|
|
1175
|
+
sql: greaterSide
|
|
1176
|
+
? `(${fieldSql} >= ${literalSql.sql} OR ${fieldSql} IS NULL${roundedNumberCandidates})`
|
|
1177
|
+
: `(${fieldSql} <= ${literalSql.sql} OR ${fieldSql} IS NULL OR ${fieldSql} >= ''${roundedNumberCandidates})`,
|
|
1178
|
+
params: literalSql.params,
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (expression.name === `eq`) {
|
|
1183
|
+
if (
|
|
1184
|
+
((left?.type === `ref` || isSafeCoalesceOperand(left)) &&
|
|
1185
|
+
right?.type === `val` &&
|
|
1186
|
+
isSafeEqualityLiteral(right.value)) ||
|
|
1187
|
+
((right?.type === `ref` || isSafeCoalesceOperand(right)) &&
|
|
1188
|
+
left?.type === `val` &&
|
|
1189
|
+
isSafeEqualityLiteral(left.value))
|
|
1190
|
+
) {
|
|
1191
|
+
return compiled
|
|
1192
|
+
}
|
|
1193
|
+
const lower =
|
|
1194
|
+
left?.type === `func` && left.name === `lower`
|
|
1195
|
+
? left
|
|
1196
|
+
: right?.type === `func` && right.name === `lower`
|
|
1197
|
+
? right
|
|
1198
|
+
: undefined
|
|
1199
|
+
const literal = lower === left ? right : left
|
|
1200
|
+
if (
|
|
1201
|
+
lower?.args.length === 1 &&
|
|
1202
|
+
lower.args[0]?.type === `ref` &&
|
|
1203
|
+
literal?.type === `val` &&
|
|
1204
|
+
typeof literal.value === `string` &&
|
|
1205
|
+
[...literal.value].every((char) => char.charCodeAt(0) <= 0x7f) &&
|
|
1206
|
+
!literal.value.includes(`\u0000`)
|
|
1207
|
+
) {
|
|
1208
|
+
const lowerSql = compileSqlExpression(lower, `index-expression`).sql
|
|
1209
|
+
return {
|
|
1210
|
+
supported: true,
|
|
1211
|
+
sql: `(${compiled.sql} OR (${lowerSql} >= ? AND ${lowerSql} GLOB '*[^ -~]*'))`,
|
|
1212
|
+
params: [...compiled.params, literal.value],
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (
|
|
1218
|
+
expression.name === `in` &&
|
|
1219
|
+
expression.args.length === 2 &&
|
|
1220
|
+
right?.type === `val` &&
|
|
1221
|
+
Array.isArray(right.value)
|
|
1222
|
+
) {
|
|
1223
|
+
if (right.value.length === 0) return compiled
|
|
1224
|
+
if (left?.type === `ref`) {
|
|
1225
|
+
if (right.value.every((value) => typeof value === `bigint`))
|
|
1226
|
+
return compiled
|
|
1227
|
+
if (right.value.every((value) => typeof value === `string`)) {
|
|
1228
|
+
// Both sides pass through SQLite's JSON decoder; one binding keeps
|
|
1229
|
+
// large string scopes within the host parameter limit, even in OR.
|
|
1230
|
+
return {
|
|
1231
|
+
supported: true,
|
|
1232
|
+
sql: `(${compileSqlExpression(left, `index-expression`).sql} IN (SELECT value FROM json_each(?)))`,
|
|
1233
|
+
params: [JSON.stringify(right.value)],
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (
|
|
1239
|
+
expression.name === `like` &&
|
|
1240
|
+
expression.args.length === 2 &&
|
|
1241
|
+
left?.type === `ref` &&
|
|
1242
|
+
right?.type === `val` &&
|
|
1243
|
+
typeof right.value === `string` &&
|
|
1244
|
+
/^[\x20-\x7e]*%$/.test(right.value) &&
|
|
1245
|
+
!right.value.slice(0, -1).includes(`%`) &&
|
|
1246
|
+
!right.value.includes(`_`)
|
|
1247
|
+
) {
|
|
1248
|
+
return compiled
|
|
1249
|
+
}
|
|
1250
|
+
return undefined
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1053
1253
|
function compileOrderByClauses(
|
|
1054
1254
|
orderBy: IR.OrderBy | undefined,
|
|
1055
1255
|
): CompiledSqlFragment {
|
|
@@ -1175,6 +1375,39 @@ function mergeObjectRows<T extends object>(existing: unknown, incoming: T): T {
|
|
|
1175
1375
|
return incoming
|
|
1176
1376
|
}
|
|
1177
1377
|
|
|
1378
|
+
function* batches<T>(
|
|
1379
|
+
changes: ReadonlyArray<T>,
|
|
1380
|
+
maxSize: number,
|
|
1381
|
+
): Generator<Array<T>> {
|
|
1382
|
+
for (let offset = 0; offset < changes.length; offset += maxSize) {
|
|
1383
|
+
yield changes.slice(offset, offset + maxSize)
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function lastMetadataByKey<
|
|
1388
|
+
T extends { type: `set` | `delete`; value?: unknown },
|
|
1389
|
+
>(
|
|
1390
|
+
changes: ReadonlyArray<T>,
|
|
1391
|
+
keyOf: (change: T) => string,
|
|
1392
|
+
undefinedSetIsNull = false,
|
|
1393
|
+
): Array<T> {
|
|
1394
|
+
const latest = new Map<string, T>()
|
|
1395
|
+
for (const change of changes) {
|
|
1396
|
+
const key = keyOf(change)
|
|
1397
|
+
const previous = latest.get(key)
|
|
1398
|
+
if (
|
|
1399
|
+
previous?.type === `set` &&
|
|
1400
|
+
(previous.value !== undefined || !undefinedSetIsNull) &&
|
|
1401
|
+
(serializePersistedRowValue(previous.value) as string | undefined) ===
|
|
1402
|
+
undefined
|
|
1403
|
+
) {
|
|
1404
|
+
throw new TypeError(`Metadata value cannot be bound to SQLite`)
|
|
1405
|
+
}
|
|
1406
|
+
latest.set(key, change)
|
|
1407
|
+
}
|
|
1408
|
+
return [...latest.values()]
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1178
1411
|
function buildIndexName(collectionId: string, signature: string): string {
|
|
1179
1412
|
const sanitizedSignature = signature
|
|
1180
1413
|
.toLowerCase()
|
|
@@ -1672,58 +1905,134 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1672
1905
|
await transactionDriver.run(`DELETE FROM ${tombstoneTableSql}`)
|
|
1673
1906
|
}
|
|
1674
1907
|
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
tx.mutations.length > 0 &&
|
|
1683
|
-
tx.mutations.every((mutation) => mutation.type !== `delete`) &&
|
|
1684
|
-
new Set(replacementKeys).size === replacementKeys.length
|
|
1685
|
-
|
|
1686
|
-
if (batchReplacement) {
|
|
1687
|
-
const finalRowMetadata = new Map<string, unknown>()
|
|
1688
|
-
for (const mutation of tx.rowMetadataMutations ?? []) {
|
|
1689
|
-
finalRowMetadata.set(
|
|
1690
|
-
encodePersistedStorageKey(mutation.key),
|
|
1691
|
-
mutation.type === `delete` ? undefined : mutation.value,
|
|
1692
|
-
)
|
|
1908
|
+
const rowMutations = new Map<string, PersistedTx[`mutations`][number]>()
|
|
1909
|
+
for (const mutation of tx.mutations) {
|
|
1910
|
+
const key = encodePersistedStorageKey(mutation.key)
|
|
1911
|
+
const previous = rowMutations.get(key)
|
|
1912
|
+
if (!previous) {
|
|
1913
|
+
rowMutations.set(key, mutation)
|
|
1914
|
+
continue
|
|
1693
1915
|
}
|
|
1694
1916
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1917
|
+
// A superseded action still had to serialize successfully.
|
|
1918
|
+
serializePersistedRowValue(previous.value)
|
|
1919
|
+
if (
|
|
1920
|
+
previous.type !== `delete` &&
|
|
1921
|
+
previous.metadataChanged === true &&
|
|
1922
|
+
previous.metadata !== undefined
|
|
1699
1923
|
) {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
)
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1924
|
+
serializePersistedRowValue(previous.metadata)
|
|
1925
|
+
}
|
|
1926
|
+
if (mutation.type === `delete`) {
|
|
1927
|
+
rowMutations.set(key, mutation)
|
|
1928
|
+
continue
|
|
1929
|
+
}
|
|
1930
|
+
const value =
|
|
1931
|
+
mutation.type === `insert` || previous.type === `delete`
|
|
1932
|
+
? mutation.value
|
|
1933
|
+
: mergeObjectRows(previous.value, mutation.value)
|
|
1934
|
+
const metadataChanged =
|
|
1935
|
+
mutation.metadataChanged === true ||
|
|
1936
|
+
previous.type === `delete` ||
|
|
1937
|
+
previous.metadataChanged === true
|
|
1938
|
+
const metadata =
|
|
1939
|
+
mutation.metadataChanged === true
|
|
1940
|
+
? mutation.metadata
|
|
1941
|
+
: previous.type === `delete`
|
|
1942
|
+
? undefined
|
|
1943
|
+
: previous.metadata
|
|
1944
|
+
const next = { key: mutation.key, value, metadataChanged, metadata }
|
|
1945
|
+
rowMutations.set(
|
|
1946
|
+
key,
|
|
1947
|
+
mutation.type === `insert` || previous.type !== `update`
|
|
1948
|
+
? { type: `insert`, ...next }
|
|
1949
|
+
: { type: `update`, ...next },
|
|
1950
|
+
)
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
for (const batch of batches(
|
|
1954
|
+
[...rowMutations.values()],
|
|
1955
|
+
this.replacementBatchSize,
|
|
1956
|
+
)) {
|
|
1957
|
+
const writes = batch.filter((mutation) => mutation.type !== `delete`)
|
|
1958
|
+
const deletes = batch.filter((mutation) => mutation.type === `delete`)
|
|
1959
|
+
const readKeys = writes
|
|
1960
|
+
.filter(
|
|
1961
|
+
(mutation) =>
|
|
1962
|
+
mutation.type === `update` || mutation.metadataChanged !== true,
|
|
1707
1963
|
)
|
|
1964
|
+
.map((mutation) => encodePersistedStorageKey(mutation.key))
|
|
1965
|
+
const existingRows =
|
|
1966
|
+
readKeys.length > 0
|
|
1967
|
+
? await transactionDriver.query<{
|
|
1968
|
+
key: string
|
|
1969
|
+
value: string
|
|
1970
|
+
metadata: string | null
|
|
1971
|
+
}>(
|
|
1972
|
+
`SELECT key, value, metadata
|
|
1973
|
+
FROM ${collectionTableSql}
|
|
1974
|
+
WHERE key IN (${readKeys.map(() => `?`).join(`, `)})`,
|
|
1975
|
+
readKeys,
|
|
1976
|
+
)
|
|
1977
|
+
: []
|
|
1978
|
+
const existing = new Map(existingRows.map((row) => [row.key, row]))
|
|
1979
|
+
const writeKeys = writes.map((mutation) =>
|
|
1980
|
+
encodePersistedStorageKey(mutation.key),
|
|
1981
|
+
)
|
|
1982
|
+
const deleteKeys = deletes.map((mutation) =>
|
|
1983
|
+
encodePersistedStorageKey(mutation.key),
|
|
1984
|
+
)
|
|
1985
|
+
|
|
1986
|
+
if (tracksPersistedKeySet && writeKeys.length > 0) {
|
|
1708
1987
|
await transactionDriver.run(
|
|
1709
1988
|
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1710
|
-
VALUES ${
|
|
1711
|
-
|
|
1989
|
+
VALUES ${writeKeys.map(() => `(?, ?)`).join(`, `)}
|
|
1990
|
+
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
1991
|
+
writeKeys.flatMap((key) => [collectionId, key]),
|
|
1712
1992
|
)
|
|
1993
|
+
}
|
|
1994
|
+
if (tracksPersistedKeySet && deleteKeys.length > 0) {
|
|
1995
|
+
await transactionDriver.run(
|
|
1996
|
+
`DELETE FROM collection_expected_keys
|
|
1997
|
+
WHERE collection_id = ? AND key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
|
|
1998
|
+
[collectionId, ...deleteKeys],
|
|
1999
|
+
)
|
|
2000
|
+
}
|
|
2001
|
+
if (deleteKeys.length > 0) {
|
|
2002
|
+
await transactionDriver.run(
|
|
2003
|
+
`DELETE FROM ${collectionTableSql}
|
|
2004
|
+
WHERE key IN (${deleteKeys.map(() => `?`).join(`, `)})`,
|
|
2005
|
+
deleteKeys,
|
|
2006
|
+
)
|
|
2007
|
+
}
|
|
2008
|
+
if (writeKeys.length > 0) {
|
|
1713
2009
|
await transactionDriver.run(
|
|
1714
2010
|
`INSERT INTO ${collectionTableSql} (key, value, metadata, row_version)
|
|
1715
|
-
VALUES ${
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2011
|
+
VALUES ${writeKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
|
|
2012
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
2013
|
+
value = excluded.value,
|
|
2014
|
+
metadata = excluded.metadata,
|
|
2015
|
+
row_version = excluded.row_version`,
|
|
2016
|
+
writes.flatMap((mutation, index) => {
|
|
2017
|
+
const previous = existing.get(writeKeys[index]!)
|
|
2018
|
+
const previousValue = previous?.value
|
|
2019
|
+
? deserializePersistedRowValue(previous.value)
|
|
2020
|
+
: undefined
|
|
2021
|
+
const previousMetadata =
|
|
2022
|
+
previous?.metadata != null
|
|
2023
|
+
? deserializePersistedRowValue(previous.metadata)
|
|
1723
2024
|
: undefined
|
|
2025
|
+
const value =
|
|
2026
|
+
mutation.type === `update`
|
|
2027
|
+
? mergeObjectRows(previousValue, mutation.value)
|
|
2028
|
+
: mutation.value
|
|
2029
|
+
const metadata =
|
|
2030
|
+
mutation.metadataChanged === true
|
|
2031
|
+
? mutation.metadata
|
|
2032
|
+
: previousMetadata
|
|
1724
2033
|
return [
|
|
1725
|
-
|
|
1726
|
-
serializePersistedRowValue(
|
|
2034
|
+
writeKeys[index]!,
|
|
2035
|
+
serializePersistedRowValue(value),
|
|
1727
2036
|
metadata === undefined
|
|
1728
2037
|
? null
|
|
1729
2038
|
: serializePersistedRowValue(metadata),
|
|
@@ -1731,143 +2040,85 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1731
2040
|
]
|
|
1732
2041
|
}),
|
|
1733
2042
|
)
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
for (const mutation of batchReplacement ? [] : tx.mutations) {
|
|
1738
|
-
const encodedKey = encodePersistedStorageKey(mutation.key)
|
|
1739
|
-
if (mutation.type === `delete`) {
|
|
1740
|
-
if (tracksPersistedKeySet) {
|
|
1741
|
-
await transactionDriver.run(
|
|
1742
|
-
`DELETE FROM collection_expected_keys
|
|
1743
|
-
WHERE collection_id = ? AND key = ?`,
|
|
1744
|
-
[collectionId, encodedKey],
|
|
1745
|
-
)
|
|
1746
|
-
}
|
|
1747
2043
|
await transactionDriver.run(
|
|
1748
|
-
`DELETE FROM ${
|
|
1749
|
-
WHERE key
|
|
1750
|
-
|
|
2044
|
+
`DELETE FROM ${tombstoneTableSql}
|
|
2045
|
+
WHERE key IN (${writeKeys.map(() => `?`).join(`, `)})`,
|
|
2046
|
+
writeKeys,
|
|
1751
2047
|
)
|
|
2048
|
+
}
|
|
2049
|
+
if (deletes.length > 0) {
|
|
1752
2050
|
await transactionDriver.run(
|
|
1753
2051
|
`INSERT INTO ${tombstoneTableSql} (key, value, row_version, deleted_at)
|
|
1754
|
-
VALUES (?, ?, ?, ?)
|
|
2052
|
+
VALUES ${deleteKeys.map(() => `(?, ?, ?, ?)`).join(`, `)}
|
|
1755
2053
|
ON CONFLICT(key) DO UPDATE SET
|
|
1756
2054
|
value = excluded.value,
|
|
1757
2055
|
row_version = excluded.row_version,
|
|
1758
2056
|
deleted_at = excluded.deleted_at`,
|
|
1759
|
-
[
|
|
1760
|
-
|
|
2057
|
+
deletes.flatMap((mutation, index) => [
|
|
2058
|
+
deleteKeys[index]!,
|
|
1761
2059
|
serializePersistedRowValue(mutation.value),
|
|
1762
2060
|
nextRowVersion,
|
|
1763
2061
|
new Date().toISOString(),
|
|
1764
|
-
],
|
|
2062
|
+
]),
|
|
1765
2063
|
)
|
|
1766
|
-
continue
|
|
1767
2064
|
}
|
|
2065
|
+
}
|
|
1768
2066
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2067
|
+
for (const batch of batches(
|
|
2068
|
+
lastMetadataByKey(
|
|
2069
|
+
tx.rowMetadataMutations ?? [],
|
|
2070
|
+
(mutation) => encodePersistedStorageKey(mutation.key),
|
|
2071
|
+
true,
|
|
2072
|
+
),
|
|
2073
|
+
this.replacementBatchSize,
|
|
2074
|
+
)) {
|
|
2075
|
+
const keys = batch.map((mutation) =>
|
|
2076
|
+
encodePersistedStorageKey(mutation.key),
|
|
1778
2077
|
)
|
|
1779
|
-
const existingValue = existingRows[0]?.value
|
|
1780
|
-
? deserializePersistedRowValue(existingRows[0].value)
|
|
1781
|
-
: undefined
|
|
1782
|
-
const existingMetadata =
|
|
1783
|
-
existingRows[0]?.metadata != null
|
|
1784
|
-
? deserializePersistedRowValue(existingRows[0].metadata)
|
|
1785
|
-
: undefined
|
|
1786
|
-
const mergedValue =
|
|
1787
|
-
mutation.type === `update`
|
|
1788
|
-
? mergeObjectRows(existingValue, mutation.value)
|
|
1789
|
-
: mutation.value
|
|
1790
|
-
const nextMetadata =
|
|
1791
|
-
mutation.metadataChanged === true
|
|
1792
|
-
? mutation.metadata
|
|
1793
|
-
: existingMetadata
|
|
1794
|
-
|
|
1795
|
-
if (tracksPersistedKeySet) {
|
|
1796
|
-
await transactionDriver.run(
|
|
1797
|
-
`INSERT INTO collection_expected_keys (collection_id, key)
|
|
1798
|
-
VALUES (?, ?)
|
|
1799
|
-
ON CONFLICT(collection_id, key) DO NOTHING`,
|
|
1800
|
-
[collectionId, encodedKey],
|
|
1801
|
-
)
|
|
1802
|
-
}
|
|
1803
2078
|
await transactionDriver.run(
|
|
1804
|
-
`
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
value = excluded.value,
|
|
1808
|
-
metadata = excluded.metadata,
|
|
1809
|
-
row_version = excluded.row_version`,
|
|
2079
|
+
`UPDATE ${collectionTableSql}
|
|
2080
|
+
SET metadata = CASE key ${keys.map(() => `WHEN ? THEN ?`).join(` `)} END
|
|
2081
|
+
WHERE key IN (${keys.map(() => `?`).join(`, `)})`,
|
|
1810
2082
|
[
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2083
|
+
...batch.flatMap((mutation, index) => [
|
|
2084
|
+
keys[index]!,
|
|
2085
|
+
mutation.type === `delete` || mutation.value === undefined
|
|
2086
|
+
? null
|
|
2087
|
+
: serializePersistedRowValue(mutation.value),
|
|
2088
|
+
]),
|
|
2089
|
+
...keys,
|
|
1817
2090
|
],
|
|
1818
2091
|
)
|
|
1819
|
-
await transactionDriver.run(
|
|
1820
|
-
`DELETE FROM ${tombstoneTableSql}
|
|
1821
|
-
WHERE key = ?`,
|
|
1822
|
-
[encodedKey],
|
|
1823
|
-
)
|
|
1824
2092
|
}
|
|
1825
2093
|
|
|
1826
|
-
for (const
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
)
|
|
1837
|
-
} else {
|
|
1838
|
-
await transactionDriver.run(
|
|
1839
|
-
`UPDATE ${collectionTableSql}
|
|
1840
|
-
SET metadata = ?
|
|
1841
|
-
WHERE key = ?`,
|
|
1842
|
-
[
|
|
1843
|
-
rowMetadataMutation.value === undefined
|
|
1844
|
-
? null
|
|
1845
|
-
: serializePersistedRowValue(rowMetadataMutation.value),
|
|
1846
|
-
encodedKey,
|
|
1847
|
-
],
|
|
1848
|
-
)
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
for (const metadataMutation of tx.collectionMetadataMutations ?? []) {
|
|
1853
|
-
if (metadataMutation.type === `delete`) {
|
|
2094
|
+
for (const batch of batches(
|
|
2095
|
+
lastMetadataByKey(
|
|
2096
|
+
tx.collectionMetadataMutations ?? [],
|
|
2097
|
+
(mutation) => mutation.key,
|
|
2098
|
+
),
|
|
2099
|
+
this.replacementBatchSize,
|
|
2100
|
+
)) {
|
|
2101
|
+
const deletes = batch.filter((mutation) => mutation.type === `delete`)
|
|
2102
|
+
const sets = batch.filter((mutation) => mutation.type === `set`)
|
|
2103
|
+
if (deletes.length > 0) {
|
|
1854
2104
|
await transactionDriver.run(
|
|
1855
2105
|
`DELETE FROM collection_metadata
|
|
1856
|
-
WHERE collection_id = ? AND key
|
|
1857
|
-
[collectionId,
|
|
2106
|
+
WHERE collection_id = ? AND key IN (${deletes.map(() => `?`).join(`, `)})`,
|
|
2107
|
+
[collectionId, ...deletes.map((mutation) => mutation.key)],
|
|
1858
2108
|
)
|
|
1859
|
-
}
|
|
2109
|
+
}
|
|
2110
|
+
if (sets.length > 0) {
|
|
1860
2111
|
await transactionDriver.run(
|
|
1861
2112
|
`INSERT INTO collection_metadata (collection_id, key, value, updated_at)
|
|
1862
|
-
VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
2113
|
+
VALUES ${sets.map(() => `(?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`).join(`, `)}
|
|
1863
2114
|
ON CONFLICT(collection_id, key) DO UPDATE SET
|
|
1864
2115
|
value = excluded.value,
|
|
1865
2116
|
updated_at = excluded.updated_at`,
|
|
1866
|
-
[
|
|
2117
|
+
sets.flatMap((mutation) => [
|
|
1867
2118
|
collectionId,
|
|
1868
|
-
|
|
1869
|
-
serializePersistedRowValue(
|
|
1870
|
-
],
|
|
2119
|
+
mutation.key,
|
|
2120
|
+
serializePersistedRowValue(mutation.value),
|
|
2121
|
+
]),
|
|
1871
2122
|
)
|
|
1872
2123
|
}
|
|
1873
2124
|
}
|
|
@@ -2419,17 +2670,22 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2419
2670
|
driver: SQLiteDriver = this.driver,
|
|
2420
2671
|
): Promise<Array<InMemoryRow<string | number, Record<string, unknown>>>> {
|
|
2421
2672
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2673
|
+
// Compile even when SQL cannot safely prefilter: invalid bindings must
|
|
2674
|
+
// reject before the in-memory fallback reads rows.
|
|
2422
2675
|
const whereCompiled = options.where
|
|
2423
2676
|
? compileSqlExpression(options.where)
|
|
2424
|
-
: { supported:
|
|
2677
|
+
: { supported: false, sql: ``, params: [] as Array<SqliteSupportedValue> }
|
|
2678
|
+
const safePrefilter = options.where
|
|
2679
|
+
? compileSafeSqlPrefilter(options.where, whereCompiled)
|
|
2680
|
+
: undefined
|
|
2425
2681
|
const orderByCompiled = compileOrderByClauses(options.orderBy)
|
|
2426
2682
|
|
|
2427
2683
|
const queryParams: Array<SqliteSupportedValue> = []
|
|
2428
2684
|
let sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`
|
|
2429
2685
|
|
|
2430
|
-
if (
|
|
2431
|
-
sql = `${sql} WHERE ${
|
|
2432
|
-
queryParams.push(...
|
|
2686
|
+
if (safePrefilter) {
|
|
2687
|
+
sql = `${sql} WHERE ${safePrefilter.sql}`
|
|
2688
|
+
queryParams.push(...safePrefilter.params)
|
|
2433
2689
|
}
|
|
2434
2690
|
|
|
2435
2691
|
if (options.orderBy && orderByCompiled.supported) {
|
|
@@ -2437,6 +2693,14 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2437
2693
|
queryParams.push(...orderByCompiled.params)
|
|
2438
2694
|
}
|
|
2439
2695
|
|
|
2696
|
+
if (
|
|
2697
|
+
queryParams.length >
|
|
2698
|
+
(driver.maxBoundParameters ?? this.driver.maxBoundParameters ?? 999)
|
|
2699
|
+
) {
|
|
2700
|
+
sql = `SELECT key, value, metadata, row_version FROM ${collectionTableSql}`
|
|
2701
|
+
queryParams.length = 0
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2440
2704
|
const storedRows = await driver.query<StoredSqliteRow>(sql, queryParams)
|
|
2441
2705
|
const parsedRows = decodeStoredSqliteRows(storedRows)
|
|
2442
2706
|
|