@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.
Files changed (255) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +7 -2
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -1
  5. package/dist/adapter-types.d.ts +9 -3
  6. package/dist/adapter-types.d.ts.map +1 -1
  7. package/dist/adapter-types.js.map +1 -1
  8. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  9. package/dist/devtools/devtools-messages-common.d.ts +7 -14
  10. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  11. package/dist/devtools/devtools-messages-common.js +1 -6
  12. package/dist/devtools/devtools-messages-common.js.map +1 -1
  13. package/dist/devtools/devtools-messages-leader.d.ts +27 -25
  14. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  15. package/dist/errors.d.ts +47 -5
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +22 -3
  18. package/dist/errors.js.map +1 -1
  19. package/dist/leader-thread/LeaderSyncProcessor.d.ts +7 -3
  20. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  21. package/dist/leader-thread/LeaderSyncProcessor.js +122 -49
  22. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  23. package/dist/leader-thread/eventlog.d.ts +4 -10
  24. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  25. package/dist/leader-thread/eventlog.js +4 -6
  26. package/dist/leader-thread/eventlog.js.map +1 -1
  27. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  28. package/dist/leader-thread/leader-worker-devtools.js +6 -2
  29. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  30. package/dist/leader-thread/make-leader-thread-layer.d.ts +1 -2
  31. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  32. package/dist/leader-thread/make-leader-thread-layer.js +68 -19
  33. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  34. package/dist/leader-thread/make-leader-thread-layer.test.d.ts +2 -0
  35. package/dist/leader-thread/make-leader-thread-layer.test.d.ts.map +1 -0
  36. package/dist/leader-thread/make-leader-thread-layer.test.js +32 -0
  37. package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -0
  38. package/dist/leader-thread/materialize-event.d.ts +2 -2
  39. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  40. package/dist/leader-thread/materialize-event.js +23 -9
  41. package/dist/leader-thread/materialize-event.js.map +1 -1
  42. package/dist/leader-thread/recreate-db.d.ts +2 -3
  43. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  44. package/dist/leader-thread/recreate-db.js +1 -1
  45. package/dist/leader-thread/recreate-db.js.map +1 -1
  46. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  47. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  48. package/dist/leader-thread/shutdown-channel.js +2 -2
  49. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  50. package/dist/leader-thread/types.d.ts +7 -5
  51. package/dist/leader-thread/types.d.ts.map +1 -1
  52. package/dist/leader-thread/types.js.map +1 -1
  53. package/dist/materializer-helper.d.ts +1 -1
  54. package/dist/materializer-helper.d.ts.map +1 -1
  55. package/dist/materializer-helper.js +20 -4
  56. package/dist/materializer-helper.js.map +1 -1
  57. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  58. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  59. package/dist/rematerialize-from-eventlog.js +25 -16
  60. package/dist/rematerialize-from-eventlog.js.map +1 -1
  61. package/dist/schema/EventDef.d.ts +3 -0
  62. package/dist/schema/EventDef.d.ts.map +1 -1
  63. package/dist/schema/EventDef.js.map +1 -1
  64. package/dist/schema/LiveStoreEvent.d.ts +1 -1
  65. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  66. package/dist/schema/LiveStoreEvent.js +1 -2
  67. package/dist/schema/LiveStoreEvent.js.map +1 -1
  68. package/dist/schema/mod.d.ts +2 -0
  69. package/dist/schema/mod.d.ts.map +1 -1
  70. package/dist/schema/mod.js +1 -0
  71. package/dist/schema/mod.js.map +1 -1
  72. package/dist/schema/schema.d.ts +15 -0
  73. package/dist/schema/schema.d.ts.map +1 -1
  74. package/dist/schema/schema.js +26 -1
  75. package/dist/schema/schema.js.map +1 -1
  76. package/dist/schema/state/sqlite/client-document-def.d.ts +35 -5
  77. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  78. package/dist/schema/state/sqlite/client-document-def.js +95 -4
  79. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  80. package/dist/schema/state/sqlite/client-document-def.test.js +16 -0
  81. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  82. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  83. package/dist/schema/state/sqlite/column-annotations.js +14 -6
  84. package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
  85. package/dist/schema/state/sqlite/column-def.d.ts +19 -0
  86. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -0
  87. package/dist/schema/state/sqlite/column-def.js +179 -0
  88. package/dist/schema/state/sqlite/column-def.js.map +1 -0
  89. package/dist/schema/state/sqlite/column-def.test.d.ts +2 -0
  90. package/dist/schema/state/sqlite/column-def.test.d.ts.map +1 -0
  91. package/dist/schema/state/sqlite/column-def.test.js +572 -0
  92. package/dist/schema/state/sqlite/column-def.test.js.map +1 -0
  93. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  94. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  95. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
  96. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  97. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  98. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
  99. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  100. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  101. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  102. package/dist/schema/state/sqlite/mod.js +1 -1
  103. package/dist/schema/state/sqlite/mod.js.map +1 -1
  104. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  105. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  106. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  107. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  108. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  109. package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
  110. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  111. package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
  112. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  113. package/dist/schema/state/sqlite/system-tables.js +2 -0
  114. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  115. package/dist/schema/state/sqlite/table-def.d.ts +6 -8
  116. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  117. package/dist/schema/state/sqlite/table-def.js +4 -211
  118. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  119. package/dist/schema/state/sqlite/table-def.test.js +59 -453
  120. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  121. package/dist/schema/unknown-events.d.ts +47 -0
  122. package/dist/schema/unknown-events.d.ts.map +1 -0
  123. package/dist/schema/unknown-events.js +69 -0
  124. package/dist/schema/unknown-events.js.map +1 -0
  125. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  126. package/dist/sql-queries/sql-query-builder.js +2 -1
  127. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  128. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
  129. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  130. package/dist/sync/ClientSessionSyncProcessor.js +35 -33
  131. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  132. package/dist/sync/errors.d.ts +61 -0
  133. package/dist/sync/errors.d.ts.map +1 -0
  134. package/dist/sync/errors.js +36 -0
  135. package/dist/sync/errors.js.map +1 -0
  136. package/dist/sync/index.d.ts +3 -0
  137. package/dist/sync/index.d.ts.map +1 -1
  138. package/dist/sync/index.js +3 -0
  139. package/dist/sync/index.js.map +1 -1
  140. package/dist/sync/mock-sync-backend.d.ts +23 -0
  141. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  142. package/dist/sync/mock-sync-backend.js +114 -0
  143. package/dist/sync/mock-sync-backend.js.map +1 -0
  144. package/dist/sync/next/compact-events.d.ts.map +1 -1
  145. package/dist/sync/next/compact-events.js +4 -5
  146. package/dist/sync/next/compact-events.js.map +1 -1
  147. package/dist/sync/next/facts.d.ts.map +1 -1
  148. package/dist/sync/next/facts.js +1 -2
  149. package/dist/sync/next/facts.js.map +1 -1
  150. package/dist/sync/next/history-dag-common.d.ts +50 -11
  151. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  152. package/dist/sync/next/history-dag-common.js +193 -4
  153. package/dist/sync/next/history-dag-common.js.map +1 -1
  154. package/dist/sync/next/history-dag.d.ts.map +1 -1
  155. package/dist/sync/next/history-dag.js +3 -1
  156. package/dist/sync/next/history-dag.js.map +1 -1
  157. package/dist/sync/sync-backend-kv.d.ts +7 -0
  158. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  159. package/dist/sync/sync-backend-kv.js +18 -0
  160. package/dist/sync/sync-backend-kv.js.map +1 -0
  161. package/dist/sync/sync-backend.d.ts +105 -0
  162. package/dist/sync/sync-backend.d.ts.map +1 -0
  163. package/dist/sync/sync-backend.js +61 -0
  164. package/dist/sync/sync-backend.js.map +1 -0
  165. package/dist/sync/sync.d.ts +6 -84
  166. package/dist/sync/sync.d.ts.map +1 -1
  167. package/dist/sync/sync.js +2 -27
  168. package/dist/sync/sync.js.map +1 -1
  169. package/dist/sync/transport-chunking.d.ts +36 -0
  170. package/dist/sync/transport-chunking.d.ts.map +1 -0
  171. package/dist/sync/transport-chunking.js +56 -0
  172. package/dist/sync/transport-chunking.js.map +1 -0
  173. package/dist/sync/validate-push-payload.d.ts +1 -1
  174. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  175. package/dist/sync/validate-push-payload.js +6 -6
  176. package/dist/sync/validate-push-payload.js.map +1 -1
  177. package/dist/testing/event-factory.d.ts +68 -0
  178. package/dist/testing/event-factory.d.ts.map +1 -0
  179. package/dist/testing/event-factory.js +80 -0
  180. package/dist/testing/event-factory.js.map +1 -0
  181. package/dist/testing/mod.d.ts +2 -0
  182. package/dist/testing/mod.d.ts.map +1 -0
  183. package/dist/testing/mod.js +2 -0
  184. package/dist/testing/mod.js.map +1 -0
  185. package/dist/version.d.ts +2 -2
  186. package/dist/version.d.ts.map +1 -1
  187. package/dist/version.js +2 -2
  188. package/dist/version.js.map +1 -1
  189. package/package.json +7 -8
  190. package/src/ClientSessionLeaderThreadProxy.ts +7 -2
  191. package/src/adapter-types.ts +13 -3
  192. package/src/devtools/devtools-messages-common.ts +1 -8
  193. package/src/errors.ts +33 -4
  194. package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
  195. package/src/leader-thread/eventlog.ts +10 -6
  196. package/src/leader-thread/leader-worker-devtools.ts +6 -2
  197. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  198. package/src/leader-thread/make-leader-thread-layer.ts +137 -26
  199. package/src/leader-thread/materialize-event.ts +34 -9
  200. package/src/leader-thread/recreate-db.ts +11 -3
  201. package/src/leader-thread/shutdown-channel.ts +16 -2
  202. package/src/leader-thread/types.ts +7 -5
  203. package/src/materializer-helper.ts +22 -5
  204. package/src/rematerialize-from-eventlog.ts +33 -23
  205. package/src/schema/EventDef.ts +3 -0
  206. package/src/schema/LiveStoreEvent.ts +1 -2
  207. package/src/schema/mod.ts +2 -0
  208. package/src/schema/schema.ts +37 -1
  209. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  210. package/src/schema/state/sqlite/client-document-def.ts +117 -5
  211. package/src/schema/state/sqlite/column-annotations.ts +16 -6
  212. package/src/schema/state/sqlite/column-def.test.ts +722 -0
  213. package/src/schema/state/sqlite/column-def.ts +215 -0
  214. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
  215. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
  216. package/src/schema/state/sqlite/mod.ts +1 -0
  217. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  218. package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
  219. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  220. package/src/schema/state/sqlite/system-tables.ts +2 -0
  221. package/src/schema/state/sqlite/table-def.test.ts +74 -569
  222. package/src/schema/state/sqlite/table-def.ts +13 -262
  223. package/src/schema/unknown-events.ts +131 -0
  224. package/src/sql-queries/sql-query-builder.ts +2 -1
  225. package/src/sync/ClientSessionSyncProcessor.ts +55 -49
  226. package/src/sync/errors.ts +38 -0
  227. package/src/sync/index.ts +3 -0
  228. package/src/sync/mock-sync-backend.ts +184 -0
  229. package/src/sync/next/compact-events.ts +4 -5
  230. package/src/sync/next/facts.ts +1 -3
  231. package/src/sync/next/history-dag-common.ts +272 -21
  232. package/src/sync/next/history-dag.ts +3 -1
  233. package/src/sync/sync-backend-kv.ts +22 -0
  234. package/src/sync/sync-backend.ts +185 -0
  235. package/src/sync/sync.ts +6 -89
  236. package/src/sync/transport-chunking.ts +90 -0
  237. package/src/sync/validate-push-payload.ts +6 -7
  238. package/src/testing/event-factory.ts +133 -0
  239. package/src/testing/mod.ts +1 -0
  240. package/src/version.ts +2 -2
  241. package/dist/schema-management/migrations.test.d.ts +0 -2
  242. package/dist/schema-management/migrations.test.d.ts.map +0 -1
  243. package/dist/schema-management/migrations.test.js +0 -52
  244. package/dist/schema-management/migrations.test.js.map +0 -1
  245. package/dist/sync/next/graphology.d.ts +0 -8
  246. package/dist/sync/next/graphology.d.ts.map +0 -1
  247. package/dist/sync/next/graphology.js +0 -30
  248. package/dist/sync/next/graphology.js.map +0 -1
  249. package/dist/sync/next/graphology_.d.ts +0 -3
  250. package/dist/sync/next/graphology_.d.ts.map +0 -1
  251. package/dist/sync/next/graphology_.js +0 -3
  252. package/dist/sync/next/graphology_.js.map +0 -1
  253. package/src/sync/next/ambient.d.ts +0 -3
  254. package/src/sync/next/graphology.ts +0 -41
  255. 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 { type Option, Schema } from '@livestore/utils/effect'
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
- * NOTE we're only including SQLite-relevant information in the hash (which excludes the schema mapping)
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
- return {
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
@@ -14,6 +14,7 @@ export {
14
14
  ClientDocumentTableDefSymbol,
15
15
  type ClientDocumentTableOptions,
16
16
  clientDocument,
17
+ createOptimisticEventSchema,
17
18
  tableIsClientDocumentTable,
18
19
  } from './client-document-def.ts'
19
20
  export * from './column-annotations.ts'
@@ -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
- default: Partial<TTableDef['Value']>
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