@livestore/common 0.4.0-dev.1 → 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 (253) 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 +6 -2
  86. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/column-def.js +122 -185
  88. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  89. package/dist/schema/state/sqlite/column-def.test.js +116 -73
  90. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  91. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -1
  92. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  93. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +23 -6
  94. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  95. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  96. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +2 -1
  97. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  98. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  99. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  100. package/dist/schema/state/sqlite/mod.js +1 -1
  101. package/dist/schema/state/sqlite/mod.js.map +1 -1
  102. package/dist/schema/state/sqlite/query-builder/api.d.ts +5 -2
  103. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  104. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  105. package/dist/schema/state/sqlite/query-builder/impl.js +6 -2
  106. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  107. package/dist/schema/state/sqlite/query-builder/impl.test.js +137 -2
  108. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  109. package/dist/schema/state/sqlite/system-tables.d.ts +42 -6
  110. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  111. package/dist/schema/state/sqlite/system-tables.js +2 -0
  112. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  113. package/dist/schema/state/sqlite/table-def.d.ts +4 -4
  114. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  115. package/dist/schema/state/sqlite/table-def.js +2 -2
  116. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  117. package/dist/schema/state/sqlite/table-def.test.js +51 -2
  118. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  119. package/dist/schema/unknown-events.d.ts +47 -0
  120. package/dist/schema/unknown-events.d.ts.map +1 -0
  121. package/dist/schema/unknown-events.js +69 -0
  122. package/dist/schema/unknown-events.js.map +1 -0
  123. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  124. package/dist/sql-queries/sql-query-builder.js +2 -1
  125. package/dist/sql-queries/sql-query-builder.js.map +1 -1
  126. package/dist/sync/ClientSessionSyncProcessor.d.ts +9 -11
  127. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  128. package/dist/sync/ClientSessionSyncProcessor.js +35 -33
  129. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  130. package/dist/sync/errors.d.ts +61 -0
  131. package/dist/sync/errors.d.ts.map +1 -0
  132. package/dist/sync/errors.js +36 -0
  133. package/dist/sync/errors.js.map +1 -0
  134. package/dist/sync/index.d.ts +3 -0
  135. package/dist/sync/index.d.ts.map +1 -1
  136. package/dist/sync/index.js +3 -0
  137. package/dist/sync/index.js.map +1 -1
  138. package/dist/sync/mock-sync-backend.d.ts +23 -0
  139. package/dist/sync/mock-sync-backend.d.ts.map +1 -0
  140. package/dist/sync/mock-sync-backend.js +114 -0
  141. package/dist/sync/mock-sync-backend.js.map +1 -0
  142. package/dist/sync/next/compact-events.d.ts.map +1 -1
  143. package/dist/sync/next/compact-events.js +4 -5
  144. package/dist/sync/next/compact-events.js.map +1 -1
  145. package/dist/sync/next/facts.d.ts.map +1 -1
  146. package/dist/sync/next/facts.js +1 -2
  147. package/dist/sync/next/facts.js.map +1 -1
  148. package/dist/sync/next/history-dag-common.d.ts +50 -11
  149. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  150. package/dist/sync/next/history-dag-common.js +193 -4
  151. package/dist/sync/next/history-dag-common.js.map +1 -1
  152. package/dist/sync/next/history-dag.d.ts.map +1 -1
  153. package/dist/sync/next/history-dag.js +3 -1
  154. package/dist/sync/next/history-dag.js.map +1 -1
  155. package/dist/sync/sync-backend-kv.d.ts +7 -0
  156. package/dist/sync/sync-backend-kv.d.ts.map +1 -0
  157. package/dist/sync/sync-backend-kv.js +18 -0
  158. package/dist/sync/sync-backend-kv.js.map +1 -0
  159. package/dist/sync/sync-backend.d.ts +105 -0
  160. package/dist/sync/sync-backend.d.ts.map +1 -0
  161. package/dist/sync/sync-backend.js +61 -0
  162. package/dist/sync/sync-backend.js.map +1 -0
  163. package/dist/sync/sync.d.ts +6 -84
  164. package/dist/sync/sync.d.ts.map +1 -1
  165. package/dist/sync/sync.js +2 -27
  166. package/dist/sync/sync.js.map +1 -1
  167. package/dist/sync/transport-chunking.d.ts +36 -0
  168. package/dist/sync/transport-chunking.d.ts.map +1 -0
  169. package/dist/sync/transport-chunking.js +56 -0
  170. package/dist/sync/transport-chunking.js.map +1 -0
  171. package/dist/sync/validate-push-payload.d.ts +1 -1
  172. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  173. package/dist/sync/validate-push-payload.js +6 -6
  174. package/dist/sync/validate-push-payload.js.map +1 -1
  175. package/dist/testing/event-factory.d.ts +68 -0
  176. package/dist/testing/event-factory.d.ts.map +1 -0
  177. package/dist/testing/event-factory.js +80 -0
  178. package/dist/testing/event-factory.js.map +1 -0
  179. package/dist/testing/mod.d.ts +2 -0
  180. package/dist/testing/mod.d.ts.map +1 -0
  181. package/dist/testing/mod.js +2 -0
  182. package/dist/testing/mod.js.map +1 -0
  183. package/dist/version.d.ts +2 -2
  184. package/dist/version.d.ts.map +1 -1
  185. package/dist/version.js +2 -2
  186. package/dist/version.js.map +1 -1
  187. package/package.json +7 -8
  188. package/src/ClientSessionLeaderThreadProxy.ts +7 -2
  189. package/src/adapter-types.ts +13 -3
  190. package/src/devtools/devtools-messages-common.ts +1 -8
  191. package/src/errors.ts +33 -4
  192. package/src/leader-thread/LeaderSyncProcessor.ts +179 -57
  193. package/src/leader-thread/eventlog.ts +10 -6
  194. package/src/leader-thread/leader-worker-devtools.ts +6 -2
  195. package/src/leader-thread/make-leader-thread-layer.test.ts +44 -0
  196. package/src/leader-thread/make-leader-thread-layer.ts +137 -26
  197. package/src/leader-thread/materialize-event.ts +34 -9
  198. package/src/leader-thread/recreate-db.ts +11 -3
  199. package/src/leader-thread/shutdown-channel.ts +16 -2
  200. package/src/leader-thread/types.ts +7 -5
  201. package/src/materializer-helper.ts +22 -5
  202. package/src/rematerialize-from-eventlog.ts +33 -23
  203. package/src/schema/EventDef.ts +3 -0
  204. package/src/schema/LiveStoreEvent.ts +1 -2
  205. package/src/schema/mod.ts +2 -0
  206. package/src/schema/schema.ts +37 -1
  207. package/src/schema/state/sqlite/client-document-def.test.ts +17 -0
  208. package/src/schema/state/sqlite/client-document-def.ts +117 -5
  209. package/src/schema/state/sqlite/column-annotations.ts +16 -6
  210. package/src/schema/state/sqlite/column-def.test.ts +150 -93
  211. package/src/schema/state/sqlite/column-def.ts +128 -203
  212. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +26 -6
  213. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +2 -1
  214. package/src/schema/state/sqlite/mod.ts +1 -0
  215. package/src/schema/state/sqlite/query-builder/api.ts +7 -2
  216. package/src/schema/state/sqlite/query-builder/impl.test.ts +187 -6
  217. package/src/schema/state/sqlite/query-builder/impl.ts +8 -2
  218. package/src/schema/state/sqlite/system-tables.ts +2 -0
  219. package/src/schema/state/sqlite/table-def.test.ts +64 -2
  220. package/src/schema/state/sqlite/table-def.ts +9 -8
  221. package/src/schema/unknown-events.ts +131 -0
  222. package/src/sql-queries/sql-query-builder.ts +2 -1
  223. package/src/sync/ClientSessionSyncProcessor.ts +55 -49
  224. package/src/sync/errors.ts +38 -0
  225. package/src/sync/index.ts +3 -0
  226. package/src/sync/mock-sync-backend.ts +184 -0
  227. package/src/sync/next/compact-events.ts +4 -5
  228. package/src/sync/next/facts.ts +1 -3
  229. package/src/sync/next/history-dag-common.ts +272 -21
  230. package/src/sync/next/history-dag.ts +3 -1
  231. package/src/sync/sync-backend-kv.ts +22 -0
  232. package/src/sync/sync-backend.ts +185 -0
  233. package/src/sync/sync.ts +6 -89
  234. package/src/sync/transport-chunking.ts +90 -0
  235. package/src/sync/validate-push-payload.ts +6 -7
  236. package/src/testing/event-factory.ts +133 -0
  237. package/src/testing/mod.ts +1 -0
  238. package/src/version.ts +2 -2
  239. package/dist/schema-management/migrations.test.d.ts +0 -2
  240. package/dist/schema-management/migrations.test.d.ts.map +0 -1
  241. package/dist/schema-management/migrations.test.js +0 -52
  242. package/dist/schema-management/migrations.test.js.map +0 -1
  243. package/dist/sync/next/graphology.d.ts +0 -8
  244. package/dist/sync/next/graphology.d.ts.map +0 -1
  245. package/dist/sync/next/graphology.js +0 -30
  246. package/dist/sync/next/graphology.js.map +0 -1
  247. package/dist/sync/next/graphology_.d.ts +0 -3
  248. package/dist/sync/next/graphology_.d.ts.map +0 -1
  249. package/dist/sync/next/graphology_.js +0 -3
  250. package/dist/sync/next/graphology_.js.map +0 -1
  251. package/src/sync/next/ambient.d.ts +0 -3
  252. package/src/sync/next/graphology.ts +0 -41
  253. package/src/sync/next/graphology_.ts +0 -2
@@ -1,4 +1,4 @@
1
- import { shouldNeverHappen, type Writeable } from '@livestore/utils'
1
+ import { shouldNeverHappen } from '@livestore/utils'
2
2
  import { Option, Schema, SchemaAST } from '@livestore/utils/effect'
3
3
 
4
4
  import { AutoIncrement, ColumnType, Default, PrimaryKeyId, Unique } from './column-annotations.ts'
@@ -6,96 +6,61 @@ import { SqliteDsl } from './db-schema/mod.ts'
6
6
 
7
7
  /**
8
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.
9
13
  */
10
14
  export const getColumnDefForSchema = (
11
15
  schema: Schema.Schema.AnyNoContext,
12
16
  propertySignature?: SchemaAST.PropertySignature,
17
+ forceNullable = false,
13
18
  ): SqliteDsl.ColumnDefinition.Any => {
14
19
  const ast = schema.ast
15
20
 
16
- // 1. Extract annotations
21
+ // Extract annotations
17
22
  const getAnnotation = <T>(annotationId: symbol): Option.Option<T> =>
18
23
  propertySignature
19
24
  ? hasPropertyAnnotation<T>(propertySignature, annotationId)
20
25
  : SchemaAST.getAnnotation<T>(annotationId)(ast)
21
26
 
22
- const annotations = {
23
- primaryKey: getAnnotation<boolean>(PrimaryKeyId).pipe(Option.getOrElse(() => false)),
24
- autoIncrement: getAnnotation<boolean>(AutoIncrement).pipe(Option.getOrElse(() => false)),
25
- defaultValue: getAnnotation<unknown>(Default),
26
- columnType: SchemaAST.getAnnotation<SqliteDsl.FieldColumnType>(ColumnType)(ast),
27
- }
27
+ const columnType = SchemaAST.getAnnotation<SqliteDsl.FieldColumnType>(ColumnType)(ast)
28
28
 
29
- // 2. Resolve the core type and nullable info
30
- const typeInfo = resolveType(ast)
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
31
 
32
- // 3. Create column definition based on resolved type
33
- let columnDef: SqliteDsl.ColumnDefinition.Any
32
+ // Get base column definition with nullable flag
33
+ const baseColumn = Option.isSome(columnType)
34
+ ? getColumnForType(columnType.value, isNullable)
35
+ : getColumnForSchema(schema, isNullable)
34
36
 
35
- // Custom column type overrides everything
36
- if (Option.isSome(annotations.columnType)) {
37
- columnDef = createColumnFromType(annotations.columnType.value, typeInfo.coreType)
38
- }
39
- // Lossy case: both null and undefined need JSON
40
- else if (typeInfo.hasNull && typeInfo.hasUndefined) {
41
- columnDef = {
42
- ...SqliteDsl.text(),
43
- nullable: true,
44
- schema: Schema.parseJson(schema),
45
- }
46
- }
47
- // Regular nullable/optional case
48
- else if (typeInfo.hasNull || typeInfo.hasUndefined) {
49
- const baseColumnDef = createColumnFromAST(typeInfo.coreType, Schema.make(typeInfo.coreType))
50
- const isComplexOptional = typeInfo.hasUndefined && !isPrimitiveAST(typeInfo.coreType)
51
-
52
- columnDef = {
53
- ...baseColumnDef,
54
- nullable: true,
55
- schema: isComplexOptional ? Schema.parseJson(schema) : schema,
56
- }
57
- }
58
- // Non-nullable type
59
- else {
60
- columnDef = createColumnFromAST(ast, schema)
61
- }
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)
62
41
 
63
- // 4. Apply annotations
64
- const result = { ...columnDef }
65
- if (annotations.primaryKey) result.primaryKey = true
66
- if (annotations.autoIncrement) result.autoIncrement = true
67
- if (Option.isSome(annotations.defaultValue)) {
68
- result.default = Option.some(annotations.defaultValue.value)
42
+ return {
43
+ ...baseColumn,
44
+ ...(primaryKey && { primaryKey: true }),
45
+ ...(autoIncrement && { autoIncrement: true }),
46
+ ...(Option.isSome(defaultValue) && { default: Option.some(defaultValue.value) }),
69
47
  }
70
-
71
- return result
72
48
  }
73
49
 
74
- /**
75
- * Checks if a property signature has a specific annotation, checking both
76
- * the property signature itself and its type AST.
77
- */
78
50
  const hasPropertyAnnotation = <T>(
79
51
  propertySignature: SchemaAST.PropertySignature,
80
52
  annotationId: symbol,
81
53
  ): Option.Option<T> => {
82
- // When using Schema.optional(Schema.String).pipe(withPrimaryKey) in a struct,
83
- // the annotation ends up on a PropertySignatureDeclaration, not the Union type
84
- // Check if this is a PropertySignatureDeclaration with annotations
85
54
  if ('annotations' in propertySignature && propertySignature.annotations) {
86
55
  const annotation = SchemaAST.getAnnotation<T>(annotationId)(propertySignature as any)
87
- if (Option.isSome(annotation)) {
88
- return annotation
89
- }
56
+ if (Option.isSome(annotation)) return annotation
90
57
  }
91
-
92
- // Otherwise check the type AST
93
58
  return SchemaAST.getAnnotation<T>(annotationId)(propertySignature.type)
94
59
  }
95
60
 
96
61
  /**
97
62
  * Maps schema property signatures to SQLite column definitions.
98
- * Returns both columns and unique column names for index creation.
63
+ * Optional fields (| undefined) become nullable columns (| null).
99
64
  */
100
65
  export const schemaFieldsToColumns = (
101
66
  propertySignatures: ReadonlyArray<SchemaAST.PropertySignature>,
@@ -104,187 +69,147 @@ export const schemaFieldsToColumns = (
104
69
  const uniqueColumns: string[] = []
105
70
 
106
71
  for (const prop of propertySignatures) {
107
- if (typeof prop.name === 'string') {
108
- // Create a schema from the AST
109
- const fieldSchema = Schema.make(prop.type)
110
- // Check if property has primary key annotation
111
- const hasPrimaryKey = hasPropertyAnnotation<boolean>(prop, PrimaryKeyId).pipe(Option.getOrElse(() => false))
112
- // Check if property has unique annotation
113
- const hasUnique = hasPropertyAnnotation<boolean>(prop, Unique).pipe(Option.getOrElse(() => false))
114
-
115
- columns[prop.name] = schemaFieldToColumn(fieldSchema, prop, hasPrimaryKey)
116
-
117
- if (hasUnique) {
118
- uniqueColumns.push(prop.name)
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`)
119
81
  }
120
82
  }
121
- }
122
-
123
- return { columns, uniqueColumns }
124
- }
125
83
 
126
- /**
127
- * Converts a schema field and its property signature to a SQLite column definition.
128
- */
129
- const schemaFieldToColumn = (
130
- fieldSchema: Schema.Schema.AnyNoContext,
131
- propertySignature: SchemaAST.PropertySignature,
132
- forceHasPrimaryKey?: boolean,
133
- ): SqliteDsl.ColumnDefinition.Any => {
134
- // Determine column type based on schema type
135
- const columnDef = getColumnDefForSchema(fieldSchema, propertySignature)
136
-
137
- // Create a new object with appropriate properties
138
- const result: Writeable<SqliteDsl.ColumnDefinition.Any> = {
139
- columnType: columnDef.columnType,
140
- schema: columnDef.schema,
141
- default: columnDef.default,
142
- nullable: columnDef.nullable,
143
- primaryKey: columnDef.primaryKey,
144
- autoIncrement: columnDef.autoIncrement,
145
- }
84
+ // Get column definition - pass nullable flag for optional fields
85
+ const columnDef = getColumnDefForSchema(fieldSchema, prop, prop.isOptional)
146
86
 
147
- // Set primaryKey property explicitly
148
- if (forceHasPrimaryKey || columnDef.primaryKey) {
149
- result.primaryKey = true
150
- } else {
151
- result.primaryKey = false
152
- }
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))
153
90
 
154
- // Check for invalid primary key + nullable combination
155
- if (result.primaryKey && (propertySignature.isOptional || columnDef.nullable)) {
156
- return shouldNeverHappen(
157
- `Primary key columns cannot be nullable. Found nullable primary key for column. ` +
158
- `Either remove the primary key annotation or use a non-nullable schema.`,
159
- )
160
- }
91
+ // Build final column
92
+ columns[prop.name] = {
93
+ ...columnDef,
94
+ ...(hasPrimaryKey && { primaryKey: true }),
95
+ }
161
96
 
162
- // Set nullable property explicitly
163
- if (propertySignature.isOptional) {
164
- result.nullable = true
165
- } else if (columnDef.nullable) {
166
- result.nullable = true
167
- } else {
168
- result.nullable = false
169
- }
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
+ }
170
102
 
171
- // Only add autoIncrement if it's true
172
- if (columnDef.autoIncrement) {
173
- result.autoIncrement = true
103
+ if (hasUnique) uniqueColumns.push(prop.name)
174
104
  }
175
105
 
176
- return result as SqliteDsl.ColumnDefinition.Any
106
+ return { columns, uniqueColumns }
177
107
  }
178
108
 
179
- /**
180
- * Resolves type information from an AST, unwrapping unions and tracking nullability.
181
- */
182
- const resolveType = (
183
- ast: SchemaAST.AST,
184
- ): {
185
- coreType: SchemaAST.AST
186
- hasNull: boolean
187
- hasUndefined: boolean
188
- } => {
189
- if (!SchemaAST.isUnion(ast)) {
190
- return { coreType: ast, hasNull: false, hasUndefined: false }
191
- }
192
-
109
+ const checkNullUndefined = (ast: SchemaAST.AST): { hasNull: boolean; hasUndefined: boolean } => {
193
110
  let hasNull = false
194
111
  let hasUndefined = false
195
- let coreType: SchemaAST.AST | undefined
196
112
 
197
113
  const visit = (type: SchemaAST.AST): void => {
198
- if (SchemaAST.isUndefinedKeyword(type)) {
199
- hasUndefined = true
200
- } else if (SchemaAST.isLiteral(type) && type.literal === null) {
201
- hasNull = true
202
- } else if (SchemaAST.isUnion(type)) {
203
- type.types.forEach(visit)
204
- } else if (!coreType) {
205
- coreType = type
206
- }
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)
207
117
  }
208
118
 
209
- ast.types.forEach(visit)
210
- return { coreType: coreType || ast, hasNull, hasUndefined }
119
+ visit(ast)
120
+ return { hasNull, hasUndefined }
211
121
  }
212
122
 
213
- /**
214
- * Creates a column definition from an AST node.
215
- */
216
- const createColumnFromAST = (
217
- ast: SchemaAST.AST,
218
- schema: Schema.Schema.AnyNoContext,
219
- ): SqliteDsl.ColumnDefinition.Any => {
220
- // Follow refinements and transformations to their core type
221
- if (SchemaAST.isRefinement(ast)) {
222
- // Special case for Schema.Int
223
- const identifier = SchemaAST.getIdentifierAnnotation(ast).pipe(Option.getOrElse(() => ''))
224
- if (identifier === 'Int') return SqliteDsl.integer()
225
- return createColumnFromAST(ast.from, Schema.make(ast.from))
226
- }
227
-
228
- if (SchemaAST.isTransformation(ast)) {
229
- return createColumnFromAST(ast.to, Schema.make(ast.to))
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))
230
127
  }
128
+ return false
129
+ }
231
130
 
232
- // Primitive types
233
- if (SchemaAST.isStringKeyword(ast)) return SqliteDsl.text()
234
- if (SchemaAST.isNumberKeyword(ast)) return SqliteDsl.real()
235
- if (SchemaAST.isBooleanKeyword(ast)) return SqliteDsl.boolean()
236
-
237
- // Literals
238
- if (SchemaAST.isLiteral(ast)) {
239
- const value = ast.literal
240
- if (typeof value === 'string') return SqliteDsl.text()
241
- if (typeof value === 'number') return SqliteDsl.real()
242
- if (typeof value === 'boolean') return SqliteDsl.boolean()
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))
243
135
  }
244
-
245
- // Everything else is complex
246
- return SqliteDsl.json({ schema })
136
+ return false
247
137
  }
248
138
 
249
- /**
250
- * Creates a column from a specific column type string.
251
- */
252
- const createColumnFromType = (columnType: string, ast: SchemaAST.AST): SqliteDsl.ColumnDefinition.Any => {
139
+ const getColumnForType = (columnType: string, nullable = false): SqliteDsl.ColumnDefinition.Any => {
253
140
  switch (columnType) {
254
141
  case 'text':
255
- return SqliteDsl.text()
142
+ return SqliteDsl.text({ nullable })
256
143
  case 'integer':
257
- // Preserve boolean transformation
258
- return SchemaAST.isBooleanKeyword(ast) ? SqliteDsl.boolean() : SqliteDsl.integer()
144
+ return SqliteDsl.integer({ nullable })
259
145
  case 'real':
260
- return SqliteDsl.real()
146
+ return SqliteDsl.real({ nullable })
261
147
  case 'blob':
262
- return SqliteDsl.blob()
148
+ return SqliteDsl.blob({ nullable })
263
149
  default:
264
150
  return shouldNeverHappen(`Unsupported column type: ${columnType}`)
265
151
  }
266
152
  }
267
153
 
268
- /**
269
- * Checks if an AST represents a primitive (non-complex) type.
270
- */
271
- const isPrimitiveAST = (ast: SchemaAST.AST): boolean => {
272
- if (
273
- SchemaAST.isStringKeyword(ast) ||
274
- SchemaAST.isNumberKeyword(ast) ||
275
- SchemaAST.isBooleanKeyword(ast) ||
276
- SchemaAST.isLiteral(ast)
277
- ) {
278
- return true
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 })
279
171
  }
280
172
 
281
- if (SchemaAST.isRefinement(ast)) {
282
- return isPrimitiveAST(ast.from)
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 })
283
180
  }
284
181
 
285
- if (SchemaAST.isTransformation(ast)) {
286
- return isPrimitiveAST(ast.to)
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 })
287
186
  }
288
187
 
289
- return false
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
290
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