@tanstack/db-sqlite-persistence-core 0.4.4 → 0.4.7
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/README.md +14 -6
- package/dist/cjs/index.cjs +5 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +12 -10
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +7 -1
- package/dist/cjs/remote-subset-wire.cjs +100 -73
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +2 -0
- package/dist/cjs/sqlite-core-adapter.cjs +136 -28
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-value.cjs +51 -0
- package/dist/cjs/sqlite-value.cjs.map +1 -1
- package/dist/cjs/sqlite-value.d.cts +12 -0
- package/dist/esm/index.js +8 -3
- package/dist/esm/persisted.d.ts +7 -1
- package/dist/esm/persisted.js +14 -12
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +2 -0
- package/dist/esm/remote-subset-wire.js +101 -74
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.js +138 -30
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/dist/esm/sqlite-value.d.ts +12 -0
- package/dist/esm/sqlite-value.js +53 -2
- package/dist/esm/sqlite-value.js.map +1 -1
- package/package.json +4 -3
- package/src/persisted.ts +31 -16
- package/src/remote-subset-wire.ts +149 -79
- package/src/sqlite-core-adapter.ts +221 -34
- package/src/sqlite-value.ts +98 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IR,
|
|
3
|
+
compareTemporalValues,
|
|
3
4
|
compileSingleRowExpression,
|
|
4
5
|
toBooleanPredicate,
|
|
5
6
|
} from '@tanstack/db'
|
|
@@ -17,7 +18,11 @@ import {
|
|
|
17
18
|
PERSISTED_TYPE_TAG,
|
|
18
19
|
PERSISTED_VALUE_TAG,
|
|
19
20
|
assertSQLiteBigIntInRange,
|
|
21
|
+
reviveSQLiteTemporal,
|
|
20
22
|
serializeSQLiteBigInt,
|
|
23
|
+
serializeSQLiteTemporal,
|
|
24
|
+
sqliteTemporalIdentity,
|
|
25
|
+
sqliteTemporalKind,
|
|
21
26
|
} from './sqlite-value'
|
|
22
27
|
import type { LoadSubsetOptions } from '@tanstack/db'
|
|
23
28
|
import type {
|
|
@@ -47,6 +52,7 @@ type CompiledSqlFragment = {
|
|
|
47
52
|
supported: boolean
|
|
48
53
|
sql: string
|
|
49
54
|
params: Array<SqliteSupportedValue>
|
|
55
|
+
identitySql?: string
|
|
50
56
|
valueKind?: CompiledValueKind
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -259,7 +265,14 @@ const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
|
259
265
|
const FORBIDDEN_SQL_FRAGMENT_PATTERN = /(;|--|\/\*)/
|
|
260
266
|
type CompiledValueKind = `unknown` | `bigint` | `date` | `datetime`
|
|
261
267
|
type PersistedTaggedValueType =
|
|
262
|
-
|
|
268
|
+
| `bigint`
|
|
269
|
+
| `date`
|
|
270
|
+
| `nan`
|
|
271
|
+
| `infinity`
|
|
272
|
+
| `-infinity`
|
|
273
|
+
| `string`
|
|
274
|
+
| `Temporal.Instant`
|
|
275
|
+
| `Temporal.PlainDate`
|
|
263
276
|
type PersistedTaggedValue = {
|
|
264
277
|
[PERSISTED_TYPE_TAG]: PersistedTaggedValueType
|
|
265
278
|
[PERSISTED_VALUE_TAG]: string
|
|
@@ -271,6 +284,9 @@ const persistedTaggedValueTypes = new Set<PersistedTaggedValueType>([
|
|
|
271
284
|
`nan`,
|
|
272
285
|
`infinity`,
|
|
273
286
|
`-infinity`,
|
|
287
|
+
`string`,
|
|
288
|
+
`Temporal.Instant`,
|
|
289
|
+
`Temporal.PlainDate`,
|
|
274
290
|
])
|
|
275
291
|
|
|
276
292
|
const orderByObjectIds = new WeakMap<object, number>()
|
|
@@ -333,6 +349,9 @@ function encodePersistedJsonValue(value: unknown): unknown {
|
|
|
333
349
|
return serializeSQLiteBigInt(value) satisfies PersistedTaggedValue
|
|
334
350
|
}
|
|
335
351
|
|
|
352
|
+
const temporal = serializeSQLiteTemporal(value)
|
|
353
|
+
if (temporal) return temporal
|
|
354
|
+
|
|
336
355
|
if (value instanceof Date) {
|
|
337
356
|
return {
|
|
338
357
|
[PERSISTED_TYPE_TAG]: `date`,
|
|
@@ -375,6 +394,13 @@ function encodePersistedJsonValue(value: unknown): unknown {
|
|
|
375
394
|
encodedRecord[key] = encodedValue
|
|
376
395
|
}
|
|
377
396
|
}
|
|
397
|
+
// Escape the marker field itself, leaving ordinary nested JSON paths intact.
|
|
398
|
+
if (typeof recordValue[PERSISTED_TYPE_TAG] === `string`) {
|
|
399
|
+
encodedRecord[PERSISTED_TYPE_TAG] = {
|
|
400
|
+
[PERSISTED_TYPE_TAG]: `string`,
|
|
401
|
+
[PERSISTED_VALUE_TAG]: recordValue[PERSISTED_TYPE_TAG],
|
|
402
|
+
}
|
|
403
|
+
}
|
|
378
404
|
return encodedRecord
|
|
379
405
|
}
|
|
380
406
|
|
|
@@ -388,6 +414,14 @@ function decodePersistedJsonValue(value: unknown): unknown {
|
|
|
388
414
|
|
|
389
415
|
if (isPersistedTaggedValue(value)) {
|
|
390
416
|
switch (value[PERSISTED_TYPE_TAG]) {
|
|
417
|
+
case `string`:
|
|
418
|
+
return value[PERSISTED_VALUE_TAG]
|
|
419
|
+
case `Temporal.Instant`:
|
|
420
|
+
case `Temporal.PlainDate`:
|
|
421
|
+
return reviveSQLiteTemporal(
|
|
422
|
+
value[PERSISTED_TYPE_TAG],
|
|
423
|
+
value[PERSISTED_VALUE_TAG],
|
|
424
|
+
)
|
|
391
425
|
case `bigint`:
|
|
392
426
|
return BigInt(value[PERSISTED_VALUE_TAG])
|
|
393
427
|
case `date`: {
|
|
@@ -459,7 +493,8 @@ function toSqliteParameterValue(value: unknown): SqliteSupportedValue {
|
|
|
459
493
|
return value.toISOString()
|
|
460
494
|
}
|
|
461
495
|
|
|
462
|
-
|
|
496
|
+
const temporal = serializeSQLiteTemporal(value)
|
|
497
|
+
return temporal?.order ?? serializePersistedRowValue(value)
|
|
463
498
|
}
|
|
464
499
|
|
|
465
500
|
function toSqliteLiteral(value: SqliteSupportedValue): string {
|
|
@@ -471,6 +506,10 @@ function toSqliteLiteral(value: SqliteSupportedValue): string {
|
|
|
471
506
|
return Number.isFinite(value) ? String(value) : `NULL`
|
|
472
507
|
}
|
|
473
508
|
|
|
509
|
+
if (value.includes(`\u0000`)) {
|
|
510
|
+
// JSON escapes NUL before SQL parsing and matches stored-string extraction.
|
|
511
|
+
return `json_extract(${toSqliteLiteral(JSON.stringify(value))}, '$')`
|
|
512
|
+
}
|
|
474
513
|
return `'${value.replace(/'/g, `''`)}'`
|
|
475
514
|
}
|
|
476
515
|
|
|
@@ -563,6 +602,10 @@ function compareOrderByValues(
|
|
|
563
602
|
return left.getTime() - right.getTime()
|
|
564
603
|
}
|
|
565
604
|
|
|
605
|
+
if (sqliteTemporalKind(left) && sqliteTemporalKind(right)) {
|
|
606
|
+
return compareTemporalValues(left, right)
|
|
607
|
+
}
|
|
608
|
+
|
|
566
609
|
const leftIsObject = typeof left === `object`
|
|
567
610
|
const rightIsObject = typeof right === `object`
|
|
568
611
|
if (leftIsObject || rightIsObject) {
|
|
@@ -720,16 +763,23 @@ function compileRefExpressionSql(jsonPath: string): CompiledSqlFragment {
|
|
|
720
763
|
const taggedValuePathSql = toSqliteLiteral(taggedValuePath)
|
|
721
764
|
const jsonPathSql = toSqliteLiteral(jsonPath)
|
|
722
765
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
766
|
+
const typeSql = `json_extract(value, ${typePathSql})`
|
|
767
|
+
const orderPathSql = toSqliteLiteral(`${jsonPath}.order`)
|
|
768
|
+
const sql = `(CASE ${typeSql}
|
|
726
769
|
WHEN 'bigint' THEN CAST(json_extract(value, ${taggedValuePathSql}) AS NUMERIC)
|
|
727
770
|
WHEN 'date' THEN json_extract(value, ${taggedValuePathSql})
|
|
771
|
+
WHEN 'string' THEN json_extract(value, ${taggedValuePathSql})
|
|
772
|
+
WHEN 'Temporal.Instant' THEN json_extract(value, ${orderPathSql})
|
|
773
|
+
WHEN 'Temporal.PlainDate' THEN json_extract(value, ${orderPathSql})
|
|
728
774
|
WHEN 'nan' THEN NULL
|
|
729
775
|
WHEN 'infinity' THEN NULL
|
|
730
776
|
WHEN '-infinity' THEN NULL
|
|
731
777
|
ELSE json_extract(value, ${jsonPathSql})
|
|
732
|
-
END)
|
|
778
|
+
END)`
|
|
779
|
+
return {
|
|
780
|
+
supported: true,
|
|
781
|
+
sql,
|
|
782
|
+
identitySql: `(CASE WHEN ${typeSql} IN ('Temporal.Instant', 'Temporal.PlainDate') THEN ${typeSql} || ':' || json_extract(value, ${taggedValuePathSql}) ELSE ${sql} END)`,
|
|
733
783
|
params: [],
|
|
734
784
|
valueKind: `unknown`,
|
|
735
785
|
}
|
|
@@ -781,6 +831,13 @@ function stableStringify(value: unknown): string {
|
|
|
781
831
|
return serializePersistedRowValue(value)
|
|
782
832
|
}
|
|
783
833
|
|
|
834
|
+
/** Balance disjunctions so a large membership index stays below SQL depth limits. */
|
|
835
|
+
function joinSqlDisjunction(parts: ReadonlyArray<string>): string {
|
|
836
|
+
if (parts.length === 1) return parts[0]!
|
|
837
|
+
const middle = Math.floor(parts.length / 2)
|
|
838
|
+
return `(${joinSqlDisjunction(parts.slice(0, middle))} OR ${joinSqlDisjunction(parts.slice(middle))})`
|
|
839
|
+
}
|
|
840
|
+
|
|
784
841
|
function argumentCompilationContext(
|
|
785
842
|
parentName: string,
|
|
786
843
|
argumentIndex: number,
|
|
@@ -824,7 +881,8 @@ function isSafeEqualityLiteral(value: unknown): boolean {
|
|
|
824
881
|
return (
|
|
825
882
|
typeof value === `string` ||
|
|
826
883
|
typeof value === `boolean` ||
|
|
827
|
-
typeof value === `bigint`
|
|
884
|
+
typeof value === `bigint` ||
|
|
885
|
+
sqliteTemporalKind(value) !== undefined
|
|
828
886
|
)
|
|
829
887
|
}
|
|
830
888
|
|
|
@@ -863,18 +921,21 @@ function compileSqlExpression(
|
|
|
863
921
|
params: [JSON.stringify(expression.value)],
|
|
864
922
|
}
|
|
865
923
|
}
|
|
866
|
-
const
|
|
924
|
+
const temporal = serializeSQLiteTemporal(expression.value)
|
|
925
|
+
const value = temporal?.order ?? toSqliteParameterValue(expression.value)
|
|
867
926
|
return {
|
|
927
|
+
identitySql: temporal
|
|
928
|
+
? toSqliteLiteral(sqliteTemporalIdentity(temporal))
|
|
929
|
+
: undefined,
|
|
868
930
|
supported: true,
|
|
869
931
|
sql:
|
|
870
932
|
context === `index-expression`
|
|
871
|
-
? toSqliteExpressionLiteral(
|
|
933
|
+
? toSqliteExpressionLiteral(
|
|
934
|
+
typeof expression.value === `bigint` ? expression.value : value,
|
|
935
|
+
)
|
|
872
936
|
: `?`,
|
|
873
|
-
params:
|
|
874
|
-
|
|
875
|
-
? [toSqliteParameterValue(expression.value)]
|
|
876
|
-
: [],
|
|
877
|
-
valueKind,
|
|
937
|
+
params: context === `predicate` ? [value] : [],
|
|
938
|
+
valueKind: getLiteralValueKind(expression.value),
|
|
878
939
|
}
|
|
879
940
|
}
|
|
880
941
|
|
|
@@ -891,15 +952,20 @@ function compileSqlExpression(
|
|
|
891
952
|
}
|
|
892
953
|
|
|
893
954
|
if (jsonPaths.length === 1) return compileRefExpressionSql(jsonPaths[0]!)
|
|
955
|
+
const variants = jsonPaths.map(compileRefExpressionSql)
|
|
894
956
|
return {
|
|
895
957
|
supported: true,
|
|
896
|
-
|
|
958
|
+
identitySql: `COALESCE(${variants.map((variant) => variant.identitySql).join(`, `)})`,
|
|
959
|
+
sql: `COALESCE(${variants.map((variant) => variant.sql).join(`, `)})`,
|
|
897
960
|
params: [],
|
|
898
961
|
valueKind: `unknown`,
|
|
899
962
|
}
|
|
900
963
|
}
|
|
901
964
|
|
|
902
|
-
|
|
965
|
+
// IN owns its list encoding; compiling that list as a scalar discards work.
|
|
966
|
+
const args =
|
|
967
|
+
expression.name === `in` ? expression.args.slice(0, 1) : expression.args
|
|
968
|
+
const compiledArgs = args.map((arg, index) =>
|
|
903
969
|
compileSqlExpression(
|
|
904
970
|
arg,
|
|
905
971
|
argumentCompilationContext(expression.name, index, arg, context),
|
|
@@ -949,18 +1015,32 @@ function compileSqlExpression(
|
|
|
949
1015
|
lte: `<=`,
|
|
950
1016
|
}
|
|
951
1017
|
|
|
1018
|
+
const comparison = compileComparisonSql(
|
|
1019
|
+
operatorByName[expression.name],
|
|
1020
|
+
expression.args[0]!,
|
|
1021
|
+
expression.args[1]!,
|
|
1022
|
+
argSql[0],
|
|
1023
|
+
argSql[1],
|
|
1024
|
+
valueKind,
|
|
1025
|
+
getCompiledValueKind(compiledArgs[0]),
|
|
1026
|
+
getCompiledValueKind(compiledArgs[1]),
|
|
1027
|
+
)
|
|
1028
|
+
// Native order ties need canonical identity, including in persisted
|
|
1029
|
+
// expressions. Ordinary string predicates keep one binding; residual
|
|
1030
|
+
// filtering removes native order-key collisions from their candidate set.
|
|
1031
|
+
const needsIdentity =
|
|
1032
|
+
expression.name === `eq` &&
|
|
1033
|
+
expression.args.every(
|
|
1034
|
+
(arg) =>
|
|
1035
|
+
arg.type !== `val` ||
|
|
1036
|
+
sqliteTemporalKind(arg.value) ||
|
|
1037
|
+
(context === `index-expression` && typeof arg.value === `string`),
|
|
1038
|
+
)
|
|
952
1039
|
return {
|
|
953
1040
|
supported: true,
|
|
954
|
-
sql:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
expression.args[1]!,
|
|
958
|
-
argSql[0],
|
|
959
|
-
argSql[1],
|
|
960
|
-
valueKind,
|
|
961
|
-
getCompiledValueKind(compiledArgs[0]),
|
|
962
|
-
getCompiledValueKind(compiledArgs[1]),
|
|
963
|
-
),
|
|
1041
|
+
sql: needsIdentity
|
|
1042
|
+
? `(${comparison} AND (${compiledArgs[0].identitySql ?? argSql[0]} = ${compiledArgs[1].identitySql ?? argSql[1]}))`
|
|
1043
|
+
: comparison,
|
|
964
1044
|
params,
|
|
965
1045
|
}
|
|
966
1046
|
}
|
|
@@ -1011,26 +1091,53 @@ function compileSqlExpression(
|
|
|
1011
1091
|
return { supported: false, sql: ``, params: [] }
|
|
1012
1092
|
}
|
|
1013
1093
|
|
|
1094
|
+
// Order and identity are two projections of one validated native encoding.
|
|
1095
|
+
const values = listValue.map((value) => {
|
|
1096
|
+
const temporal = serializeSQLiteTemporal(value)
|
|
1097
|
+
return {
|
|
1098
|
+
value:
|
|
1099
|
+
typeof value === `bigint`
|
|
1100
|
+
? assertSQLiteBigIntInRange(value)
|
|
1101
|
+
: (temporal?.order ?? toSqliteParameterValue(value)),
|
|
1102
|
+
identity: temporal ? sqliteTemporalIdentity(temporal) : undefined,
|
|
1103
|
+
}
|
|
1104
|
+
})
|
|
1105
|
+
let identity = ``
|
|
1106
|
+
if (
|
|
1107
|
+
listValue.some(
|
|
1108
|
+
(value) =>
|
|
1109
|
+
sqliteTemporalKind(value) ||
|
|
1110
|
+
(context === `index-expression` && typeof value === `string`),
|
|
1111
|
+
)
|
|
1112
|
+
) {
|
|
1113
|
+
const leftIdentity = compiledArgs[0]?.identitySql ?? leftSql
|
|
1114
|
+
// Index expressions cannot contain subqueries. Both forms preserve
|
|
1115
|
+
// correlated membership and SQL's three-valued Boolean semantics.
|
|
1116
|
+
identity =
|
|
1117
|
+
context === `index-expression`
|
|
1118
|
+
? ` AND (${joinSqlDisjunction(values.map(({ value, identity: nativeIdentity }) => `(${leftSql} = ${toSqliteExpressionLiteral(value)} AND ${leftIdentity} = ${toSqliteExpressionLiteral(nativeIdentity ?? value)})`))})`
|
|
1119
|
+
: ` AND ((${leftSql}, ${leftIdentity}) IN (VALUES ${[...new Set(values.map(({ value, identity: nativeIdentity }) => `(${toSqliteExpressionLiteral(value)}, ${toSqliteExpressionLiteral(nativeIdentity ?? value)})`))].join(`, `)}))`
|
|
1120
|
+
}
|
|
1014
1121
|
if (context === `index-expression`) {
|
|
1015
1122
|
return {
|
|
1016
1123
|
supported: true,
|
|
1017
|
-
sql: `(${leftSql} IN (${
|
|
1018
|
-
.map((value) => toSqliteExpressionLiteral(value))
|
|
1019
|
-
.join(`, `)}))`,
|
|
1124
|
+
sql: `(${leftSql} IN (${values
|
|
1125
|
+
.map(({ value }) => toSqliteExpressionLiteral(value))
|
|
1126
|
+
.join(`, `)})${identity})`,
|
|
1020
1127
|
params: leftParams,
|
|
1021
1128
|
}
|
|
1022
1129
|
}
|
|
1023
1130
|
|
|
1024
|
-
const jsonList = `[${
|
|
1025
|
-
.map((value) =>
|
|
1131
|
+
const jsonList = `[${values
|
|
1132
|
+
.map(({ value }) =>
|
|
1026
1133
|
typeof value === `bigint`
|
|
1027
1134
|
? assertSQLiteBigIntInRange(value).toString()
|
|
1028
|
-
: JSON.stringify(
|
|
1135
|
+
: JSON.stringify(value),
|
|
1029
1136
|
)
|
|
1030
1137
|
.join(`,`)}]`
|
|
1031
1138
|
return {
|
|
1032
1139
|
supported: true,
|
|
1033
|
-
sql: `(${leftSql} IN (SELECT value FROM json_each(?)))`,
|
|
1140
|
+
sql: `(${leftSql} IN (SELECT value FROM json_each(?))${identity})`,
|
|
1034
1141
|
params: [...leftParams, jsonList],
|
|
1035
1142
|
}
|
|
1036
1143
|
}
|
|
@@ -1058,7 +1165,12 @@ function compileSqlExpression(
|
|
|
1058
1165
|
case `concat`:
|
|
1059
1166
|
return { supported: true, sql: `(${argSql.join(` || `)})`, params }
|
|
1060
1167
|
case `coalesce`:
|
|
1061
|
-
return {
|
|
1168
|
+
return {
|
|
1169
|
+
supported: true,
|
|
1170
|
+
sql: `COALESCE(${argSql.join(`, `)})`,
|
|
1171
|
+
identitySql: `COALESCE(${compiledArgs.map((arg) => arg.identitySql ?? arg.sql).join(`, `)})`,
|
|
1172
|
+
params,
|
|
1173
|
+
}
|
|
1062
1174
|
case `add`:
|
|
1063
1175
|
return { supported: true, sql: `(${argSql[0]} + ${argSql[1]})`, params }
|
|
1064
1176
|
case `subtract`:
|
|
@@ -1096,6 +1208,51 @@ function compileSqlExpression(
|
|
|
1096
1208
|
}
|
|
1097
1209
|
}
|
|
1098
1210
|
|
|
1211
|
+
/** A raw ref/native-literal leaf has exact native truth before prefilter broadening. */
|
|
1212
|
+
function nativeComparisonRef(
|
|
1213
|
+
expression: IR.BasicExpression,
|
|
1214
|
+
): IR.PropRef | undefined {
|
|
1215
|
+
if (expression.type !== `func` || expression.args.length !== 2)
|
|
1216
|
+
return undefined
|
|
1217
|
+
const [left, right] = expression.args
|
|
1218
|
+
if (expression.name === `in`) {
|
|
1219
|
+
return left?.type === `ref` &&
|
|
1220
|
+
right?.type === `val` &&
|
|
1221
|
+
Array.isArray(right.value) &&
|
|
1222
|
+
right.value.length > 0 &&
|
|
1223
|
+
right.value.every((value) => sqliteTemporalKind(value))
|
|
1224
|
+
? left
|
|
1225
|
+
: undefined
|
|
1226
|
+
}
|
|
1227
|
+
if (![`eq`, `gt`, `gte`, `lt`, `lte`].includes(expression.name))
|
|
1228
|
+
return undefined
|
|
1229
|
+
if (
|
|
1230
|
+
left?.type === `ref` &&
|
|
1231
|
+
right?.type === `val` &&
|
|
1232
|
+
sqliteTemporalKind(right.value)
|
|
1233
|
+
)
|
|
1234
|
+
return left
|
|
1235
|
+
if (
|
|
1236
|
+
right?.type === `ref` &&
|
|
1237
|
+
left?.type === `val` &&
|
|
1238
|
+
sqliteTemporalKind(left.value)
|
|
1239
|
+
)
|
|
1240
|
+
return right
|
|
1241
|
+
return undefined
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function compileNativeRefGuard(ref: IR.PropRef): string {
|
|
1245
|
+
const paths = createJsonPathVariants(
|
|
1246
|
+
IR.getPropRefPropertyPath(ref).map(String),
|
|
1247
|
+
)!
|
|
1248
|
+
const kinds = paths.map(
|
|
1249
|
+
(path) =>
|
|
1250
|
+
`json_extract(value, ${toSqliteLiteral(`${path}.${PERSISTED_TYPE_TAG}`)})`,
|
|
1251
|
+
)
|
|
1252
|
+
const kind = kinds.length === 1 ? kinds[0] : `COALESCE(${kinds.join(`, `)})`
|
|
1253
|
+
return `${kind} IN ('Temporal.Instant', 'Temporal.PlainDate')`
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1099
1256
|
function compileSafeSqlPrefilter(
|
|
1100
1257
|
expression: IR.BasicExpression,
|
|
1101
1258
|
compiled?: CompiledSqlFragment,
|
|
@@ -1139,7 +1296,37 @@ function compileSafeSqlPrefilter(
|
|
|
1139
1296
|
compiled ??= compileSqlExpression(expression)
|
|
1140
1297
|
if (!compiled.supported) return undefined
|
|
1141
1298
|
const [left, right] = expression.args
|
|
1299
|
+
if (
|
|
1300
|
+
expression.name === `not` &&
|
|
1301
|
+
expression.args.length === 1 &&
|
|
1302
|
+
left &&
|
|
1303
|
+
nativeComparisonRef(left)
|
|
1304
|
+
) {
|
|
1305
|
+
// Negate the original leaf, never a broadened prefilter. Nonfinite stored
|
|
1306
|
+
// scalars project to NULL; retaining them also retains valid NOT matches.
|
|
1307
|
+
return { ...compiled, sql: `COALESCE(${compiled.sql}, 1)` }
|
|
1308
|
+
}
|
|
1142
1309
|
if (expression.args.length === 2) {
|
|
1310
|
+
const nativeRef = nativeComparisonRef(expression)
|
|
1311
|
+
if (nativeRef) {
|
|
1312
|
+
if (expression.name === `eq` || expression.name === `in`) return compiled
|
|
1313
|
+
// NaN orders above native values in the core evaluator. NULL candidates
|
|
1314
|
+
// also preserve that law with either operand direction.
|
|
1315
|
+
const fieldSql = compileSqlExpression(nativeRef, `index-expression`).sql
|
|
1316
|
+
return { ...compiled, sql: `(${compiled.sql} OR ${fieldSql} IS NULL)` }
|
|
1317
|
+
}
|
|
1318
|
+
if (
|
|
1319
|
+
expression.name === `eq` &&
|
|
1320
|
+
left?.type === `ref` &&
|
|
1321
|
+
right?.type === `ref`
|
|
1322
|
+
) {
|
|
1323
|
+
// Ref/ref scalar coercions are not generally SQL-exact. Restrict only
|
|
1324
|
+
// native pairs; leave every other pair to the authoritative evaluator.
|
|
1325
|
+
return {
|
|
1326
|
+
...compiled,
|
|
1327
|
+
sql: `(CASE WHEN ${compileNativeRefGuard(left)} AND ${compileNativeRefGuard(right)} THEN ${compiled.sql} ELSE 1 END)`,
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1143
1330
|
if (
|
|
1144
1331
|
[`gt`, `gte`, `lt`, `lte`].includes(expression.name) &&
|
|
1145
1332
|
(left?.type === `ref` || right?.type === `ref`)
|
package/src/sqlite-value.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
InvalidPersistedCollectionConfigError,
|
|
3
|
+
SQLiteBigIntOutOfRangeError,
|
|
4
|
+
} from './errors'
|
|
2
5
|
|
|
3
6
|
export const SQLITE_BIGINT_MIN = -9_223_372_036_854_775_808n
|
|
4
7
|
export const SQLITE_BIGINT_MAX = 9_223_372_036_854_775_807n
|
|
@@ -28,3 +31,97 @@ export function serializeSQLiteBigInt(value: bigint): SerializedSQLiteBigInt {
|
|
|
28
31
|
[PERSISTED_VALUE_TAG]: value.toString(),
|
|
29
32
|
}
|
|
30
33
|
}
|
|
34
|
+
|
|
35
|
+
type SQLiteTemporalKind = `Temporal.Instant` | `Temporal.PlainDate`
|
|
36
|
+
type SQLiteTemporal = {
|
|
37
|
+
epochNanoseconds: bigint
|
|
38
|
+
year: number
|
|
39
|
+
month: number
|
|
40
|
+
day: number
|
|
41
|
+
withCalendar: (calendar: string) => SQLiteTemporal
|
|
42
|
+
}
|
|
43
|
+
type TemporalConstructor = {
|
|
44
|
+
from: (value: string) => SQLiteTemporal
|
|
45
|
+
prototype: { toString: () => string }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SerializedSQLiteTemporal = {
|
|
49
|
+
[PERSISTED_TYPE_TAG]: SQLiteTemporalKind
|
|
50
|
+
[PERSISTED_VALUE_TAG]: string
|
|
51
|
+
order: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sqliteTemporalKind(
|
|
55
|
+
value: unknown,
|
|
56
|
+
): SQLiteTemporalKind | undefined {
|
|
57
|
+
if (value === null || typeof value !== `object`) return
|
|
58
|
+
const tag = (value as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]
|
|
59
|
+
if (tag === `Temporal.Instant` || tag === `Temporal.PlainDate`) return tag
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requireSQLiteTemporalConstructor(
|
|
64
|
+
kind: SQLiteTemporalKind,
|
|
65
|
+
): TemporalConstructor {
|
|
66
|
+
const name = kind.slice(`Temporal.`.length)
|
|
67
|
+
const constructor = (
|
|
68
|
+
globalThis as { Temporal?: Record<string, TemporalConstructor> }
|
|
69
|
+
).Temporal?.[name]
|
|
70
|
+
if (
|
|
71
|
+
typeof constructor?.from !== `function` ||
|
|
72
|
+
typeof constructor.prototype.toString !== `function`
|
|
73
|
+
) {
|
|
74
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
75
|
+
`Missing global ${kind} constructor for SQLite persistence`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return constructor
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function reviveSQLiteTemporal(
|
|
82
|
+
kind: SQLiteTemporalKind,
|
|
83
|
+
value: string,
|
|
84
|
+
): unknown {
|
|
85
|
+
return requireSQLiteTemporalConstructor(kind).from(value)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Registered prototype methods validate the brand; user toString is ignored. */
|
|
89
|
+
export function serializeSQLiteTemporal(
|
|
90
|
+
value: unknown,
|
|
91
|
+
): SerializedSQLiteTemporal | undefined {
|
|
92
|
+
if (value === null || typeof value !== `object`) return
|
|
93
|
+
const prototype = Object.getPrototypeOf(value)
|
|
94
|
+
if (prototype === null || prototype === Object.prototype) return
|
|
95
|
+
const tag = (value as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]
|
|
96
|
+
if (typeof tag !== `string` || !tag.startsWith(`Temporal.`)) return
|
|
97
|
+
const kind = sqliteTemporalKind(value)
|
|
98
|
+
if (!kind)
|
|
99
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
100
|
+
`Unsupported ${tag} value for SQLite persistence`,
|
|
101
|
+
)
|
|
102
|
+
const constructor = requireSQLiteTemporalConstructor(kind)
|
|
103
|
+
let text: string
|
|
104
|
+
try {
|
|
105
|
+
text = constructor.prototype.toString.call(value)
|
|
106
|
+
} catch {
|
|
107
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
108
|
+
`Invalid ${kind} value for the registered global constructor`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
const native = constructor.from(text)
|
|
112
|
+
const iso =
|
|
113
|
+
kind === `Temporal.PlainDate` ? native.withCalendar(`iso8601`) : undefined
|
|
114
|
+
// Text keys avoid both floating-point precision loss and SQLite's int64 cap.
|
|
115
|
+
const order = iso
|
|
116
|
+
? `${String(iso.year + 1_000_000).padStart(7, `0`)}${String(iso.month).padStart(2, `0`)}${String(iso.day).padStart(2, `0`)}`
|
|
117
|
+
: (native.epochNanoseconds + 10_000_000_000_000_000_000_000n)
|
|
118
|
+
.toString()
|
|
119
|
+
.padStart(23, `0`)
|
|
120
|
+
return { [PERSISTED_TYPE_TAG]: kind, [PERSISTED_VALUE_TAG]: text, order }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function sqliteTemporalIdentity(
|
|
124
|
+
value: SerializedSQLiteTemporal,
|
|
125
|
+
): string {
|
|
126
|
+
return `${value[PERSISTED_TYPE_TAG]}:${value[PERSISTED_VALUE_TAG]}`
|
|
127
|
+
}
|