@livestore/common 0.4.0-dev.0 → 0.4.0-dev.10
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/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts +7 -2
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
- package/dist/adapter-types.d.ts +9 -3
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +7 -14
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +1 -6
- package/dist/devtools/devtools-messages-common.js.map +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +27 -25
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +47 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +22 -3
- package/dist/errors.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +122 -49
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +4 -10
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +4 -6
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +6 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +68 -19
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
- package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
- package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
- package/dist/leader-thread/materialize-event.d.ts +2 -2
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +23 -9
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +2 -3
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -1
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.d.ts +2 -2
- package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +2 -2
- package/dist/leader-thread/shutdown-channel.js.map +1 -1
- package/dist/leader-thread/types.d.ts +7 -5
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/materializer-helper.d.ts +1 -1
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +20 -4
- package/dist/materializer-helper.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +1 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +25 -16
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +3 -0
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts +1 -1
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +1 -2
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/mod.d.ts +2 -0
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +1 -0
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +15 -0
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +26 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +95 -4
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.js +14 -6
- package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts +19 -0
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-def.js +179 -0
- package/dist/schema/state/sqlite/column-def.js.map +1 -0
- package/dist/schema/state/sqlite/column-def.test.d.ts +2 -0
- package/dist/schema/state/sqlite/column-def.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/column-def.test.js +572 -0
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +1 -1
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +2 -0
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +6 -8
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +4 -211
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +59 -453
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts +47 -0
- package/dist/schema/unknown-events.d.ts.map +1 -0
- package/dist/schema/unknown-events.js +69 -0
- package/dist/schema/unknown-events.js.map +1 -0
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js +2 -1
- package/dist/sql-queries/sql-query-builder.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +35 -33
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/errors.d.ts +61 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +36 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/index.d.ts +3 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +3 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mock-sync-backend.d.ts +23 -0
- package/dist/sync/mock-sync-backend.d.ts.map +1 -0
- package/dist/sync/mock-sync-backend.js +114 -0
- package/dist/sync/mock-sync-backend.js.map +1 -0
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +4 -5
- package/dist/sync/next/compact-events.js.map +1 -1
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +1 -2
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +50 -11
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +193 -4
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +3 -1
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/sync-backend-kv.d.ts +7 -0
- package/dist/sync/sync-backend-kv.d.ts.map +1 -0
- package/dist/sync/sync-backend-kv.js +18 -0
- package/dist/sync/sync-backend-kv.js.map +1 -0
- package/dist/sync/sync-backend.d.ts +105 -0
- package/dist/sync/sync-backend.d.ts.map +1 -0
- package/dist/sync/sync-backend.js +61 -0
- package/dist/sync/sync-backend.js.map +1 -0
- package/dist/sync/sync.d.ts +6 -84
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +2 -27
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/transport-chunking.d.ts +36 -0
- package/dist/sync/transport-chunking.d.ts.map +1 -0
- package/dist/sync/transport-chunking.js +56 -0
- package/dist/sync/transport-chunking.js.map +1 -0
- package/dist/sync/validate-push-payload.d.ts +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +6 -6
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/testing/event-factory.d.ts +68 -0
- package/dist/testing/event-factory.d.ts.map +1 -0
- package/dist/testing/event-factory.js +80 -0
- package/dist/testing/event-factory.js.map +1 -0
- package/dist/testing/mod.d.ts +2 -0
- package/dist/testing/mod.d.ts.map +1 -0
- package/dist/testing/mod.js +2 -0
- package/dist/testing/mod.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +7 -8
- package/src/ClientSessionLeaderThreadProxy.ts +7 -2
- package/src/adapter-types.ts +13 -3
- package/src/devtools/devtools-messages-common.ts +1 -8
- package/src/errors.ts +33 -4
- package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
- package/src/leader-thread/eventlog.ts +10 -6
- package/src/leader-thread/leader-worker-devtools.ts +6 -2
- package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
- package/src/leader-thread/make-leader-thread-layer.ts +137 -26
- package/src/leader-thread/materialize-event.ts +34 -9
- package/src/leader-thread/recreate-db.ts +11 -3
- package/src/leader-thread/shutdown-channel.ts +16 -2
- package/src/leader-thread/types.ts +7 -5
- package/src/materializer-helper.ts +22 -5
- package/src/rematerialize-from-eventlog.ts +33 -23
- package/src/schema/EventDef.ts +3 -0
- package/src/schema/LiveStoreEvent.ts +1 -2
- package/src/schema/mod.ts +2 -0
- package/src/schema/schema.ts +37 -1
- package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
- package/src/schema/state/sqlite/client-document-def.ts +117 -5
- package/src/schema/state/sqlite/column-annotations.ts +16 -6
- package/src/schema/state/sqlite/column-def.test.ts +722 -0
- package/src/schema/state/sqlite/column-def.ts +215 -0
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
- package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
- package/src/schema/state/sqlite/mod.ts +1 -0
- package/src/schema/state/sqlite/query-builder/api.ts +7 -2
- package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
- package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
- package/src/schema/state/sqlite/system-tables.ts +2 -0
- package/src/schema/state/sqlite/table-def.test.ts +74 -569
- package/src/schema/state/sqlite/table-def.ts +13 -262
- package/src/schema/unknown-events.ts +131 -0
- package/src/sql-queries/sql-query-builder.ts +2 -1
- package/src/sync/ClientSessionSyncProcessor.ts +55 -49
- package/src/sync/errors.ts +38 -0
- package/src/sync/index.ts +3 -0
- package/src/sync/mock-sync-backend.ts +184 -0
- package/src/sync/next/compact-events.ts +4 -5
- package/src/sync/next/facts.ts +1 -3
- package/src/sync/next/history-dag-common.ts +272 -21
- package/src/sync/next/history-dag.ts +3 -1
- package/src/sync/sync-backend-kv.ts +22 -0
- package/src/sync/sync-backend.ts +185 -0
- package/src/sync/sync.ts +6 -89
- package/src/sync/transport-chunking.ts +90 -0
- package/src/sync/validate-push-payload.ts +6 -7
- package/src/testing/event-factory.ts +133 -0
- package/src/testing/mod.ts +1 -0
- package/src/version.ts +2 -2
- package/dist/schema-management/migrations.test.d.ts +0 -2
- package/dist/schema-management/migrations.test.d.ts.map +0 -1
- package/dist/schema-management/migrations.test.js +0 -52
- package/dist/schema-management/migrations.test.js.map +0 -1
- package/dist/sync/next/graphology.d.ts +0 -8
- package/dist/sync/next/graphology.d.ts.map +0 -1
- package/dist/sync/next/graphology.js +0 -30
- package/dist/sync/next/graphology.js.map +0 -1
- package/dist/sync/next/graphology_.d.ts +0 -3
- package/dist/sync/next/graphology_.d.ts.map +0 -1
- package/dist/sync/next/graphology_.js +0 -3
- package/dist/sync/next/graphology_.js.map +0 -1
- package/src/sync/next/ambient.d.ts +0 -3
- package/src/sync/next/graphology.ts +0 -41
- package/src/sync/next/graphology_.ts +0 -2
@@ -0,0 +1,215 @@
|
|
1
|
+
import { shouldNeverHappen } from '@livestore/utils'
|
2
|
+
import { Option, Schema, SchemaAST } from '@livestore/utils/effect'
|
3
|
+
|
4
|
+
import { AutoIncrement, ColumnType, Default, PrimaryKeyId, Unique } from './column-annotations.ts'
|
5
|
+
import { SqliteDsl } from './db-schema/mod.ts'
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Maps a schema to a SQLite column definition, respecting column annotations.
|
9
|
+
*
|
10
|
+
* Note: When used with schema-based table definitions, optional fields (| undefined)
|
11
|
+
* are transformed to nullable fields (| null) to match SQLite's NULL semantics.
|
12
|
+
* Fields with both null and undefined will emit a warning as this is a lossy conversion.
|
13
|
+
*/
|
14
|
+
export const getColumnDefForSchema = (
|
15
|
+
schema: Schema.Schema.AnyNoContext,
|
16
|
+
propertySignature?: SchemaAST.PropertySignature,
|
17
|
+
forceNullable = false,
|
18
|
+
): SqliteDsl.ColumnDefinition.Any => {
|
19
|
+
const ast = schema.ast
|
20
|
+
|
21
|
+
// Extract annotations
|
22
|
+
const getAnnotation = <T>(annotationId: symbol): Option.Option<T> =>
|
23
|
+
propertySignature
|
24
|
+
? hasPropertyAnnotation<T>(propertySignature, annotationId)
|
25
|
+
: SchemaAST.getAnnotation<T>(annotationId)(ast)
|
26
|
+
|
27
|
+
const columnType = SchemaAST.getAnnotation<SqliteDsl.FieldColumnType>(ColumnType)(ast)
|
28
|
+
|
29
|
+
// Check if schema has null (e.g., Schema.NullOr) or undefined or if it's forced nullable (optional field)
|
30
|
+
const isNullable = forceNullable || hasNull(ast) || hasUndefined(ast)
|
31
|
+
|
32
|
+
// Get base column definition with nullable flag
|
33
|
+
const baseColumn = Option.isSome(columnType)
|
34
|
+
? getColumnForType(columnType.value, isNullable)
|
35
|
+
: getColumnForSchema(schema, isNullable)
|
36
|
+
|
37
|
+
// Apply annotations
|
38
|
+
const primaryKey = getAnnotation<boolean>(PrimaryKeyId).pipe(Option.getOrElse(() => false))
|
39
|
+
const autoIncrement = getAnnotation<boolean>(AutoIncrement).pipe(Option.getOrElse(() => false))
|
40
|
+
const defaultValue = getAnnotation<unknown>(Default)
|
41
|
+
|
42
|
+
return {
|
43
|
+
...baseColumn,
|
44
|
+
...(primaryKey && { primaryKey: true }),
|
45
|
+
...(autoIncrement && { autoIncrement: true }),
|
46
|
+
...(Option.isSome(defaultValue) && { default: Option.some(defaultValue.value) }),
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
const hasPropertyAnnotation = <T>(
|
51
|
+
propertySignature: SchemaAST.PropertySignature,
|
52
|
+
annotationId: symbol,
|
53
|
+
): Option.Option<T> => {
|
54
|
+
if ('annotations' in propertySignature && propertySignature.annotations) {
|
55
|
+
const annotation = SchemaAST.getAnnotation<T>(annotationId)(propertySignature as any)
|
56
|
+
if (Option.isSome(annotation)) return annotation
|
57
|
+
}
|
58
|
+
return SchemaAST.getAnnotation<T>(annotationId)(propertySignature.type)
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Maps schema property signatures to SQLite column definitions.
|
63
|
+
* Optional fields (| undefined) become nullable columns (| null).
|
64
|
+
*/
|
65
|
+
export const schemaFieldsToColumns = (
|
66
|
+
propertySignatures: ReadonlyArray<SchemaAST.PropertySignature>,
|
67
|
+
): { columns: SqliteDsl.Columns; uniqueColumns: string[] } => {
|
68
|
+
const columns: SqliteDsl.Columns = {}
|
69
|
+
const uniqueColumns: string[] = []
|
70
|
+
|
71
|
+
for (const prop of propertySignatures) {
|
72
|
+
if (typeof prop.name !== 'string') continue
|
73
|
+
|
74
|
+
const fieldSchema = Schema.make(prop.type)
|
75
|
+
|
76
|
+
// Warn about lossy conversion for fields with both null and undefined
|
77
|
+
if (prop.isOptional) {
|
78
|
+
const { hasNull, hasUndefined } = checkNullUndefined(fieldSchema.ast)
|
79
|
+
if (hasNull && hasUndefined) {
|
80
|
+
console.warn(`Field '${prop.name}' has both null and undefined - treating | undefined as | null`)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
// Get column definition - pass nullable flag for optional fields
|
85
|
+
const columnDef = getColumnDefForSchema(fieldSchema, prop, prop.isOptional)
|
86
|
+
|
87
|
+
// Check for primary key and unique annotations
|
88
|
+
const hasPrimaryKey = hasPropertyAnnotation<boolean>(prop, PrimaryKeyId).pipe(Option.getOrElse(() => false))
|
89
|
+
const hasUnique = hasPropertyAnnotation<boolean>(prop, Unique).pipe(Option.getOrElse(() => false))
|
90
|
+
|
91
|
+
// Build final column
|
92
|
+
columns[prop.name] = {
|
93
|
+
...columnDef,
|
94
|
+
...(hasPrimaryKey && { primaryKey: true }),
|
95
|
+
}
|
96
|
+
|
97
|
+
// Validate primary key + nullable
|
98
|
+
const column = columns[prop.name]
|
99
|
+
if (column?.primaryKey && column.nullable) {
|
100
|
+
throw new Error('Primary key columns cannot be nullable')
|
101
|
+
}
|
102
|
+
|
103
|
+
if (hasUnique) uniqueColumns.push(prop.name)
|
104
|
+
}
|
105
|
+
|
106
|
+
return { columns, uniqueColumns }
|
107
|
+
}
|
108
|
+
|
109
|
+
const checkNullUndefined = (ast: SchemaAST.AST): { hasNull: boolean; hasUndefined: boolean } => {
|
110
|
+
let hasNull = false
|
111
|
+
let hasUndefined = false
|
112
|
+
|
113
|
+
const visit = (type: SchemaAST.AST): void => {
|
114
|
+
if (SchemaAST.isUndefinedKeyword(type)) hasUndefined = true
|
115
|
+
else if (SchemaAST.isLiteral(type) && type.literal === null) hasNull = true
|
116
|
+
else if (SchemaAST.isUnion(type)) type.types.forEach(visit)
|
117
|
+
}
|
118
|
+
|
119
|
+
visit(ast)
|
120
|
+
return { hasNull, hasUndefined }
|
121
|
+
}
|
122
|
+
|
123
|
+
const hasNull = (ast: SchemaAST.AST): boolean => {
|
124
|
+
if (SchemaAST.isLiteral(ast) && ast.literal === null) return true
|
125
|
+
if (SchemaAST.isUnion(ast)) {
|
126
|
+
return ast.types.some((type) => hasNull(type))
|
127
|
+
}
|
128
|
+
return false
|
129
|
+
}
|
130
|
+
|
131
|
+
const hasUndefined = (ast: SchemaAST.AST): boolean => {
|
132
|
+
if (SchemaAST.isUndefinedKeyword(ast)) return true
|
133
|
+
if (SchemaAST.isUnion(ast)) {
|
134
|
+
return ast.types.some((type) => hasUndefined(type))
|
135
|
+
}
|
136
|
+
return false
|
137
|
+
}
|
138
|
+
|
139
|
+
const getColumnForType = (columnType: string, nullable = false): SqliteDsl.ColumnDefinition.Any => {
|
140
|
+
switch (columnType) {
|
141
|
+
case 'text':
|
142
|
+
return SqliteDsl.text({ nullable })
|
143
|
+
case 'integer':
|
144
|
+
return SqliteDsl.integer({ nullable })
|
145
|
+
case 'real':
|
146
|
+
return SqliteDsl.real({ nullable })
|
147
|
+
case 'blob':
|
148
|
+
return SqliteDsl.blob({ nullable })
|
149
|
+
default:
|
150
|
+
return shouldNeverHappen(`Unsupported column type: ${columnType}`)
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
const getColumnForSchema = (schema: Schema.Schema.AnyNoContext, nullable = false): SqliteDsl.ColumnDefinition.Any => {
|
155
|
+
const ast = schema.ast
|
156
|
+
// Strip nullable wrapper to get core type
|
157
|
+
const coreAst = stripNullable(ast)
|
158
|
+
const coreSchema = stripNullable(ast) === ast ? schema : Schema.make(coreAst)
|
159
|
+
|
160
|
+
// Special case: Boolean is transformed to integer in SQLite
|
161
|
+
if (SchemaAST.isBooleanKeyword(coreAst)) {
|
162
|
+
return SqliteDsl.boolean({ nullable })
|
163
|
+
}
|
164
|
+
|
165
|
+
// Get the encoded AST - what actually gets stored in SQLite
|
166
|
+
const encodedAst = Schema.encodedSchema(coreSchema).ast
|
167
|
+
|
168
|
+
// Check if the encoded type matches SQLite native types
|
169
|
+
if (SchemaAST.isStringKeyword(encodedAst)) {
|
170
|
+
return SqliteDsl.text({ schema: coreSchema, nullable })
|
171
|
+
}
|
172
|
+
|
173
|
+
if (SchemaAST.isNumberKeyword(encodedAst)) {
|
174
|
+
// Special cases for integer columns
|
175
|
+
const id = SchemaAST.getIdentifierAnnotation(coreAst).pipe(Option.getOrElse(() => ''))
|
176
|
+
if (id === 'Int' || id === 'DateFromNumber') {
|
177
|
+
return SqliteDsl.integer({ schema: coreSchema, nullable })
|
178
|
+
}
|
179
|
+
return SqliteDsl.real({ schema: coreSchema, nullable })
|
180
|
+
}
|
181
|
+
|
182
|
+
// Literals based on their type
|
183
|
+
if (SchemaAST.isLiteral(coreAst)) {
|
184
|
+
const value = coreAst.literal
|
185
|
+
if (typeof value === 'boolean') return SqliteDsl.boolean({ nullable })
|
186
|
+
}
|
187
|
+
|
188
|
+
// Literals based on their encoded type
|
189
|
+
if (SchemaAST.isLiteral(encodedAst)) {
|
190
|
+
const value = encodedAst.literal
|
191
|
+
if (typeof value === 'string') return SqliteDsl.text({ schema: coreSchema, nullable })
|
192
|
+
if (typeof value === 'number') {
|
193
|
+
// Check if the original schema is Int
|
194
|
+
const id = SchemaAST.getIdentifierAnnotation(coreAst).pipe(Option.getOrElse(() => ''))
|
195
|
+
if (id === 'Int') {
|
196
|
+
return SqliteDsl.integer({ schema: coreSchema, nullable })
|
197
|
+
}
|
198
|
+
return SqliteDsl.real({ schema: coreSchema, nullable })
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
// Everything else needs JSON encoding
|
203
|
+
return SqliteDsl.json({ schema: coreSchema, nullable })
|
204
|
+
}
|
205
|
+
|
206
|
+
const stripNullable = (ast: SchemaAST.AST): SchemaAST.AST => {
|
207
|
+
if (!SchemaAST.isUnion(ast)) return ast
|
208
|
+
|
209
|
+
// Find non-null/undefined type
|
210
|
+
const core = ast.types.find(
|
211
|
+
(type) => !(SchemaAST.isLiteral(type) && type.literal === null) && !SchemaAST.isUndefinedKeyword(type),
|
212
|
+
)
|
213
|
+
|
214
|
+
return core || ast
|
215
|
+
}
|
@@ -1,4 +1,5 @@
|
|
1
|
-
import {
|
1
|
+
import { omitUndefineds } from '@livestore/utils'
|
2
|
+
import { type Option, Schema, SchemaAST } from '@livestore/utils/effect'
|
2
3
|
|
3
4
|
import { hashCode } from '../hash.ts'
|
4
5
|
|
@@ -45,9 +46,7 @@ export const index = (
|
|
45
46
|
): Index => ({
|
46
47
|
_tag: 'index',
|
47
48
|
columns,
|
48
|
-
name,
|
49
|
-
unique,
|
50
|
-
primaryKey,
|
49
|
+
...omitUndefineds({ name, unique, primaryKey }),
|
51
50
|
})
|
52
51
|
|
53
52
|
export type ForeignKey = {
|
@@ -85,7 +84,19 @@ export type DbSchema = {
|
|
85
84
|
export const dbSchema = (tables: Table[]): DbSchema => ({ _tag: 'dbSchema', tables })
|
86
85
|
|
87
86
|
/**
|
88
|
-
*
|
87
|
+
* Helper to detect if a column is a JSON column (has parseJson transformation)
|
88
|
+
*/
|
89
|
+
const isJsonColumn = (column: Column): boolean => {
|
90
|
+
if (column.type._tag !== 'text') return false
|
91
|
+
|
92
|
+
// Check if the schema AST is a parseJson transformation
|
93
|
+
const ast = column.schema.ast
|
94
|
+
return ast._tag === 'Transformation' && ast.annotations.schemaId === SchemaAST.ParseJsonSchemaId
|
95
|
+
}
|
96
|
+
|
97
|
+
/**
|
98
|
+
* NOTE we're now including JSON schema information for JSON columns
|
99
|
+
* to detect client document schema changes
|
89
100
|
*/
|
90
101
|
export const hash = (obj: Table | Column | Index | ForeignKey | DbSchema): number =>
|
91
102
|
hashCode(JSON.stringify(trimInfoForHasing(obj)))
|
@@ -101,7 +112,7 @@ const trimInfoForHasing = (obj: Table | Column | Index | ForeignKey | DbSchema):
|
|
101
112
|
}
|
102
113
|
}
|
103
114
|
case 'column': {
|
104
|
-
|
115
|
+
const baseInfo: Record<string, any> = {
|
105
116
|
_tag: 'column',
|
106
117
|
name: obj.name,
|
107
118
|
type: obj.type._tag,
|
@@ -110,6 +121,15 @@ const trimInfoForHasing = (obj: Table | Column | Index | ForeignKey | DbSchema):
|
|
110
121
|
autoIncrement: obj.autoIncrement,
|
111
122
|
default: obj.default,
|
112
123
|
}
|
124
|
+
|
125
|
+
// NEW: Include schema hash for JSON columns
|
126
|
+
// This ensures that changes to the JSON schema are detected
|
127
|
+
if (isJsonColumn(obj) && obj.schema) {
|
128
|
+
// Use Effect's Schema.hash for consistent hashing
|
129
|
+
baseInfo.jsonSchemaHash = Schema.hash(obj.schema)
|
130
|
+
}
|
131
|
+
|
132
|
+
return baseInfo
|
113
133
|
}
|
114
134
|
case 'index': {
|
115
135
|
return {
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import type { Nullable } from '@livestore/utils'
|
2
|
+
import { omitUndefineds } from '@livestore/utils'
|
2
3
|
import type { Option, Types } from '@livestore/utils/effect'
|
3
4
|
import { Schema } from '@livestore/utils/effect'
|
4
5
|
|
@@ -46,7 +47,7 @@ export const table = <TTableName extends string, TColumns extends Columns, TInde
|
|
46
47
|
indexes: indexesToAst(indexes ?? []),
|
47
48
|
}
|
48
49
|
|
49
|
-
return { name, columns, indexes, ast }
|
50
|
+
return { name, columns, ...omitUndefineds({ indexes }), ast }
|
50
51
|
}
|
51
52
|
|
52
53
|
export type AnyIfConstained<In, Out> = '__constrained' extends keyof In ? any : Out
|
@@ -3,7 +3,7 @@ import { type Option, Predicate, type Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
4
4
|
import type { SessionIdSymbol } from '../../../../adapter-types.ts'
|
5
5
|
import type { SqlValue } from '../../../../util.ts'
|
6
|
-
import type { ClientDocumentTableDef } from '../client-document-def.ts'
|
6
|
+
import type { ClientDocumentTableDef, ClientDocumentTableDefSymbol } from '../client-document-def.ts'
|
7
7
|
import type { SqliteDsl } from '../db-schema/mod.ts'
|
8
8
|
import type { TableDefBase } from '../table-def.ts'
|
9
9
|
|
@@ -437,7 +437,12 @@ export namespace QueryBuilder {
|
|
437
437
|
|
438
438
|
export namespace RowQuery {
|
439
439
|
export type GetOrCreateOptions<TTableDef extends ClientDocumentTableDef.TraitAny> = {
|
440
|
-
|
440
|
+
/**
|
441
|
+
* Default value to use instead of the default value from the table definition
|
442
|
+
*/
|
443
|
+
default: TTableDef[ClientDocumentTableDefSymbol]['options']['partialSet'] extends false
|
444
|
+
? TTableDef['Value']
|
445
|
+
: Partial<TTableDef['Value']>
|
441
446
|
}
|
442
447
|
|
443
448
|
// TODO get rid of this
|
@@ -409,9 +409,7 @@ describe('query builder', () => {
|
|
409
409
|
})
|
410
410
|
|
411
411
|
it('should handle INSERT queries with undefined values', () => {
|
412
|
-
expect(
|
413
|
-
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active', completed: undefined })),
|
414
|
-
).toMatchInlineSnapshot(`
|
412
|
+
expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }))).toMatchInlineSnapshot(`
|
415
413
|
{
|
416
414
|
"bindValues": [
|
417
415
|
"123",
|
@@ -478,9 +476,7 @@ describe('query builder', () => {
|
|
478
476
|
})
|
479
477
|
|
480
478
|
it('should handle UPDATE queries with undefined values', () => {
|
481
|
-
expect(
|
482
|
-
dump(db.todos.update({ status: undefined, text: 'some text' }).where({ id: '123' })),
|
483
|
-
).toMatchInlineSnapshot(`
|
479
|
+
expect(dump(db.todos.update({ text: 'some text' }).where({ id: '123' }))).toMatchInlineSnapshot(`
|
484
480
|
{
|
485
481
|
"bindValues": [
|
486
482
|
"some text",
|
@@ -622,6 +618,191 @@ describe('query builder', () => {
|
|
622
618
|
}
|
623
619
|
`)
|
624
620
|
})
|
621
|
+
|
622
|
+
it('should handle where().delete() - preserving where clauses', () => {
|
623
|
+
expect(dump(db.todos.where({ status: 'completed' }).delete())).toMatchInlineSnapshot(`
|
624
|
+
{
|
625
|
+
"bindValues": [
|
626
|
+
"completed",
|
627
|
+
],
|
628
|
+
"query": "DELETE FROM 'todos' WHERE status = ?",
|
629
|
+
"schema": "number",
|
630
|
+
}
|
631
|
+
`)
|
632
|
+
|
633
|
+
// Multiple where clauses
|
634
|
+
expect(dump(db.todos.where({ status: 'completed' }).where({ deletedAt: null }).delete())).toMatchInlineSnapshot(`
|
635
|
+
{
|
636
|
+
"bindValues": [
|
637
|
+
"completed",
|
638
|
+
],
|
639
|
+
"query": "DELETE FROM 'todos' WHERE status = ? AND deletedAt IS NULL",
|
640
|
+
"schema": "number",
|
641
|
+
}
|
642
|
+
`)
|
643
|
+
})
|
644
|
+
|
645
|
+
it('should handle where().update() - preserving where clauses', () => {
|
646
|
+
expect(dump(db.todos.where({ id: '123' }).update({ status: 'completed' }))).toMatchInlineSnapshot(`
|
647
|
+
{
|
648
|
+
"bindValues": [
|
649
|
+
"completed",
|
650
|
+
"123",
|
651
|
+
],
|
652
|
+
"query": "UPDATE 'todos' SET status = ? WHERE id = ?",
|
653
|
+
"schema": "number",
|
654
|
+
}
|
655
|
+
`)
|
656
|
+
|
657
|
+
// Multiple where clauses
|
658
|
+
expect(
|
659
|
+
dump(db.todos.where({ id: '123' }).where({ deletedAt: null }).update({ status: 'completed' })),
|
660
|
+
).toMatchInlineSnapshot(`
|
661
|
+
{
|
662
|
+
"bindValues": [
|
663
|
+
"completed",
|
664
|
+
"123",
|
665
|
+
],
|
666
|
+
"query": "UPDATE 'todos' SET status = ? WHERE id = ? AND deletedAt IS NULL",
|
667
|
+
"schema": "number",
|
668
|
+
}
|
669
|
+
`)
|
670
|
+
})
|
671
|
+
|
672
|
+
it('should have equivalent behavior for both delete patterns', () => {
|
673
|
+
const pattern1 = dump(db.todos.where({ status: 'completed', id: '123' }).delete())
|
674
|
+
const pattern2 = dump(db.todos.delete().where({ status: 'completed', id: '123' }))
|
675
|
+
|
676
|
+
expect(pattern1).toEqual(pattern2)
|
677
|
+
})
|
678
|
+
|
679
|
+
it('should have equivalent behavior for both update patterns', () => {
|
680
|
+
const pattern1 = dump(db.todos.where({ id: '123' }).update({ status: 'completed', text: 'Updated' }))
|
681
|
+
const pattern2 = dump(db.todos.update({ status: 'completed', text: 'Updated' }).where({ id: '123' }))
|
682
|
+
|
683
|
+
expect(pattern1).toEqual(pattern2)
|
684
|
+
})
|
685
|
+
})
|
686
|
+
|
687
|
+
describe('schema transforms', () => {
|
688
|
+
const Flat = Schema.Struct({
|
689
|
+
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
|
690
|
+
contactFirstName: Schema.String,
|
691
|
+
contactLastName: Schema.String,
|
692
|
+
contactEmail: Schema.String.pipe(State.SQLite.withUnique),
|
693
|
+
})
|
694
|
+
|
695
|
+
const Nested = Schema.transform(
|
696
|
+
Flat,
|
697
|
+
Schema.Struct({
|
698
|
+
id: Schema.String,
|
699
|
+
contact: Schema.Struct({
|
700
|
+
firstName: Schema.String,
|
701
|
+
lastName: Schema.String,
|
702
|
+
email: Schema.String,
|
703
|
+
}),
|
704
|
+
}),
|
705
|
+
{
|
706
|
+
decode: ({ id, contactFirstName, contactLastName, contactEmail }) => ({
|
707
|
+
id,
|
708
|
+
contact: {
|
709
|
+
firstName: contactFirstName,
|
710
|
+
lastName: contactLastName,
|
711
|
+
email: contactEmail,
|
712
|
+
},
|
713
|
+
}),
|
714
|
+
encode: ({ id, contact }) => ({
|
715
|
+
id,
|
716
|
+
contactFirstName: contact.firstName,
|
717
|
+
contactLastName: contact.lastName,
|
718
|
+
contactEmail: contact.email,
|
719
|
+
}),
|
720
|
+
},
|
721
|
+
)
|
722
|
+
|
723
|
+
const makeContactsTable = () =>
|
724
|
+
State.SQLite.table({
|
725
|
+
name: 'contacts',
|
726
|
+
schema: Nested,
|
727
|
+
// schema: Flat,
|
728
|
+
})
|
729
|
+
|
730
|
+
it('exposes flattened insert type while schema type is nested', () => {
|
731
|
+
const contactsTable = makeContactsTable()
|
732
|
+
|
733
|
+
type InsertInput = Parameters<(typeof contactsTable)['insert']>[0]
|
734
|
+
type NestedType = Schema.Schema.Type<typeof Nested>
|
735
|
+
|
736
|
+
type Assert<T extends true> = T
|
737
|
+
|
738
|
+
type InsertKeys = keyof InsertInput
|
739
|
+
type NestedKeys = keyof NestedType
|
740
|
+
|
741
|
+
type _InsertHasFlattenedColumns = Assert<
|
742
|
+
'contactFirstName' extends InsertKeys
|
743
|
+
? 'contactLastName' extends InsertKeys
|
744
|
+
? 'contactEmail' extends InsertKeys
|
745
|
+
? true
|
746
|
+
: false
|
747
|
+
: false
|
748
|
+
: false
|
749
|
+
>
|
750
|
+
|
751
|
+
type _InsertDoesNotExposeNested = Assert<Extract<'contact', InsertKeys> extends never ? true : false>
|
752
|
+
|
753
|
+
type _SchemaTypeIsNested = Assert<'contact' extends NestedKeys ? true : false>
|
754
|
+
|
755
|
+
void contactsTable
|
756
|
+
})
|
757
|
+
|
758
|
+
it('fails to encode nested inserts because flat columns are required', () => {
|
759
|
+
const contactsTable = makeContactsTable()
|
760
|
+
|
761
|
+
expect(
|
762
|
+
contactsTable
|
763
|
+
// TODO in the future we should use decoded types here instead of encoded
|
764
|
+
.insert({
|
765
|
+
id: 'person-1',
|
766
|
+
contactFirstName: 'Ada',
|
767
|
+
contactLastName: 'Lovelace',
|
768
|
+
contactEmail: 'ada@example.com',
|
769
|
+
})
|
770
|
+
.asSql(),
|
771
|
+
).toMatchInlineSnapshot(`
|
772
|
+
{
|
773
|
+
"bindValues": [
|
774
|
+
"person-1",
|
775
|
+
"Ada",
|
776
|
+
"Lovelace",
|
777
|
+
"ada@example.com",
|
778
|
+
],
|
779
|
+
"query": "INSERT INTO 'contacts' (id, contactFirstName, contactLastName, contactEmail) VALUES (?, ?, ?, ?)",
|
780
|
+
"usedTables": Set {
|
781
|
+
"contacts",
|
782
|
+
},
|
783
|
+
}
|
784
|
+
`)
|
785
|
+
})
|
786
|
+
|
787
|
+
it('fails to encode nested inserts because flat columns are required', () => {
|
788
|
+
const contactsTable = makeContactsTable()
|
789
|
+
|
790
|
+
expect(() =>
|
791
|
+
contactsTable
|
792
|
+
.insert({
|
793
|
+
id: 'person-1',
|
794
|
+
// @ts-expect-error
|
795
|
+
contact: {
|
796
|
+
firstName: 'Ada',
|
797
|
+
lastName: 'Lovelace',
|
798
|
+
email: 'ada@example.com',
|
799
|
+
},
|
800
|
+
})
|
801
|
+
.asSql(),
|
802
|
+
).toThrowErrorMatchingInlineSnapshot(`
|
803
|
+
[ParseError: contacts\n└─ ["contactFirstName"]\n └─ is missing]
|
804
|
+
`)
|
805
|
+
})
|
625
806
|
})
|
626
807
|
})
|
627
808
|
|
@@ -219,21 +219,27 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
|
|
219
219
|
update: (values) => {
|
220
220
|
const filteredValues = Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined))
|
221
221
|
|
222
|
+
// Preserve where clauses if coming from a SelectQuery
|
223
|
+
const whereClause = ast._tag === 'SelectQuery' ? ast.where : []
|
224
|
+
|
222
225
|
return makeQueryBuilder(tableDef, {
|
223
226
|
_tag: 'UpdateQuery',
|
224
227
|
tableDef,
|
225
228
|
values: filteredValues,
|
226
|
-
where:
|
229
|
+
where: whereClause,
|
227
230
|
returning: undefined,
|
228
231
|
resultSchema: Schema.Void,
|
229
232
|
}) as any
|
230
233
|
},
|
231
234
|
|
232
235
|
delete: () => {
|
236
|
+
// Preserve where clauses if coming from a SelectQuery
|
237
|
+
const whereClause = ast._tag === 'SelectQuery' ? ast.where : []
|
238
|
+
|
233
239
|
return makeQueryBuilder(tableDef, {
|
234
240
|
_tag: 'DeleteQuery',
|
235
241
|
tableDef,
|
236
|
-
where:
|
242
|
+
where: whereClause,
|
237
243
|
returning: undefined,
|
238
244
|
resultSchema: Schema.Void,
|
239
245
|
}) as any
|
@@ -96,6 +96,8 @@ export const syncStatusTable = table({
|
|
96
96
|
name: SYNC_STATUS_TABLE,
|
97
97
|
columns: {
|
98
98
|
head: SqliteDsl.integer({ primaryKey: true }),
|
99
|
+
// Null means the sync backend is not yet connected and we haven't yet seen a backend ID
|
100
|
+
backendId: SqliteDsl.text({ nullable: true }),
|
99
101
|
},
|
100
102
|
})
|
101
103
|
|