@livestore/common 0.3.2-dev.9 → 0.4.0-dev.1

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 (172) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +2 -2
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/adapter-types.d.ts +4 -4
  5. package/dist/adapter-types.d.ts.map +1 -1
  6. package/dist/debug-info.d.ts +17 -17
  7. package/dist/devtools/devtools-messages-client-session.d.ts +38 -38
  8. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  9. package/dist/devtools/devtools-messages-leader.d.ts +28 -28
  10. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  11. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  12. package/dist/leader-thread/LeaderSyncProcessor.js +3 -1
  13. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  14. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  15. package/dist/leader-thread/make-leader-thread-layer.js +21 -4
  16. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  17. package/dist/leader-thread/shutdown-channel.d.ts +2 -2
  18. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  19. package/dist/leader-thread/shutdown-channel.js +2 -2
  20. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  21. package/dist/leader-thread/types.d.ts +1 -1
  22. package/dist/leader-thread/types.d.ts.map +1 -1
  23. package/dist/materializer-helper.d.ts +3 -3
  24. package/dist/materializer-helper.d.ts.map +1 -1
  25. package/dist/materializer-helper.js +2 -2
  26. package/dist/materializer-helper.js.map +1 -1
  27. package/dist/rematerialize-from-eventlog.js +1 -1
  28. package/dist/rematerialize-from-eventlog.js.map +1 -1
  29. package/dist/schema/EventDef.d.ts +104 -178
  30. package/dist/schema/EventSequenceNumber.d.ts +5 -0
  31. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  32. package/dist/schema/EventSequenceNumber.js +7 -2
  33. package/dist/schema/EventSequenceNumber.js.map +1 -1
  34. package/dist/schema/EventSequenceNumber.test.js +2 -2
  35. package/dist/schema/LiveStoreEvent.d.ts +6 -5
  36. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  37. package/dist/schema/LiveStoreEvent.js +5 -0
  38. package/dist/schema/LiveStoreEvent.js.map +1 -1
  39. package/dist/schema/schema.d.ts +3 -0
  40. package/dist/schema/schema.d.ts.map +1 -1
  41. package/dist/schema/schema.js.map +1 -1
  42. package/dist/schema/state/sqlite/client-document-def.d.ts +3 -2
  43. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  44. package/dist/schema/state/sqlite/client-document-def.js +6 -4
  45. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  46. package/dist/schema/state/sqlite/client-document-def.test.js +76 -1
  47. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  48. package/dist/schema/state/sqlite/column-annotations.d.ts +34 -0
  49. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -0
  50. package/dist/schema/state/sqlite/column-annotations.js +50 -0
  51. package/dist/schema/state/sqlite/column-annotations.js.map +1 -0
  52. package/dist/schema/state/sqlite/column-annotations.test.d.ts +2 -0
  53. package/dist/schema/state/sqlite/column-annotations.test.d.ts.map +1 -0
  54. package/dist/schema/state/sqlite/column-annotations.test.js +179 -0
  55. package/dist/schema/state/sqlite/column-annotations.test.js.map +1 -0
  56. package/dist/schema/state/sqlite/column-def.d.ts +15 -0
  57. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -0
  58. package/dist/schema/state/sqlite/column-def.js +242 -0
  59. package/dist/schema/state/sqlite/column-def.js.map +1 -0
  60. package/dist/schema/state/sqlite/column-def.test.d.ts +2 -0
  61. package/dist/schema/state/sqlite/column-def.test.d.ts.map +1 -0
  62. package/dist/schema/state/sqlite/column-def.test.js +529 -0
  63. package/dist/schema/state/sqlite/column-def.test.js.map +1 -0
  64. package/dist/schema/state/sqlite/column-spec.d.ts +11 -0
  65. package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -0
  66. package/dist/schema/state/sqlite/column-spec.js +39 -0
  67. package/dist/schema/state/sqlite/column-spec.js.map +1 -0
  68. package/dist/schema/state/sqlite/column-spec.test.d.ts +2 -0
  69. package/dist/schema/state/sqlite/column-spec.test.d.ts.map +1 -0
  70. package/dist/schema/state/sqlite/column-spec.test.js +146 -0
  71. package/dist/schema/state/sqlite/column-spec.test.js.map +1 -0
  72. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -0
  73. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  74. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -0
  75. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  76. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +17 -4
  77. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
  78. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -0
  79. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  80. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +65 -165
  81. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  82. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -0
  83. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  84. package/dist/schema/state/sqlite/mod.d.ts +2 -0
  85. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  86. package/dist/schema/state/sqlite/mod.js +2 -0
  87. package/dist/schema/state/sqlite/mod.js.map +1 -1
  88. package/dist/schema/state/sqlite/query-builder/api.d.ts +309 -560
  89. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  90. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts +1 -0
  91. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
  92. package/dist/schema/state/sqlite/query-builder/astToSql.js +8 -6
  93. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
  94. package/dist/schema/state/sqlite/system-tables.d.ts +464 -46
  95. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  96. package/dist/schema/state/sqlite/table-def.d.ts +159 -152
  97. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  98. package/dist/schema/state/sqlite/table-def.js +45 -6
  99. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  100. package/dist/schema/state/sqlite/table-def.test.d.ts +2 -0
  101. package/dist/schema/state/sqlite/table-def.test.d.ts.map +1 -0
  102. package/dist/schema/state/sqlite/table-def.test.js +192 -0
  103. package/dist/schema/state/sqlite/table-def.test.js.map +1 -0
  104. package/dist/schema-management/common.d.ts +1 -1
  105. package/dist/schema-management/common.d.ts.map +1 -1
  106. package/dist/schema-management/common.js +11 -2
  107. package/dist/schema-management/common.js.map +1 -1
  108. package/dist/schema-management/migrations.d.ts +0 -1
  109. package/dist/schema-management/migrations.d.ts.map +1 -1
  110. package/dist/schema-management/migrations.js +4 -30
  111. package/dist/schema-management/migrations.js.map +1 -1
  112. package/dist/schema-management/migrations.test.d.ts +2 -0
  113. package/dist/schema-management/migrations.test.d.ts.map +1 -0
  114. package/dist/schema-management/migrations.test.js +52 -0
  115. package/dist/schema-management/migrations.test.js.map +1 -0
  116. package/dist/sql-queries/types.d.ts +37 -133
  117. package/dist/sqlite-db-helper.d.ts +3 -1
  118. package/dist/sqlite-db-helper.d.ts.map +1 -1
  119. package/dist/sqlite-db-helper.js +16 -0
  120. package/dist/sqlite-db-helper.js.map +1 -1
  121. package/dist/sqlite-types.d.ts +4 -4
  122. package/dist/sqlite-types.d.ts.map +1 -1
  123. package/dist/sync/ClientSessionSyncProcessor.d.ts +2 -2
  124. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  125. package/dist/sync/ClientSessionSyncProcessor.js +8 -7
  126. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  127. package/dist/sync/sync.d.ts.map +1 -1
  128. package/dist/sync/sync.js.map +1 -1
  129. package/dist/util.d.ts +3 -3
  130. package/dist/util.d.ts.map +1 -1
  131. package/dist/util.js.map +1 -1
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.js +1 -1
  134. package/package.json +4 -4
  135. package/src/ClientSessionLeaderThreadProxy.ts +2 -2
  136. package/src/adapter-types.ts +6 -4
  137. package/src/devtools/devtools-messages-leader.ts +3 -3
  138. package/src/leader-thread/LeaderSyncProcessor.ts +3 -1
  139. package/src/leader-thread/make-leader-thread-layer.ts +26 -7
  140. package/src/leader-thread/shutdown-channel.ts +2 -2
  141. package/src/leader-thread/types.ts +1 -1
  142. package/src/materializer-helper.ts +5 -11
  143. package/src/rematerialize-from-eventlog.ts +2 -2
  144. package/src/schema/EventSequenceNumber.test.ts +2 -2
  145. package/src/schema/EventSequenceNumber.ts +8 -2
  146. package/src/schema/LiveStoreEvent.ts +7 -1
  147. package/src/schema/schema.ts +4 -0
  148. package/src/schema/state/sqlite/client-document-def.test.ts +89 -1
  149. package/src/schema/state/sqlite/client-document-def.ts +7 -4
  150. package/src/schema/state/sqlite/column-annotations.test.ts +212 -0
  151. package/src/schema/state/sqlite/column-annotations.ts +77 -0
  152. package/src/schema/state/sqlite/column-def.test.ts +665 -0
  153. package/src/schema/state/sqlite/column-def.ts +290 -0
  154. package/src/schema/state/sqlite/column-spec.test.ts +223 -0
  155. package/src/schema/state/sqlite/column-spec.ts +42 -0
  156. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +2 -0
  157. package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +15 -0
  158. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +20 -2
  159. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +1 -0
  160. package/src/schema/state/sqlite/mod.ts +2 -0
  161. package/src/schema/state/sqlite/query-builder/api.ts +4 -3
  162. package/src/schema/state/sqlite/query-builder/astToSql.ts +9 -7
  163. package/src/schema/state/sqlite/table-def.test.ts +241 -0
  164. package/src/schema/state/sqlite/table-def.ts +222 -16
  165. package/src/schema-management/common.ts +10 -3
  166. package/src/schema-management/migrations.ts +4 -33
  167. package/src/sqlite-db-helper.ts +19 -1
  168. package/src/sqlite-types.ts +4 -4
  169. package/src/sync/ClientSessionSyncProcessor.ts +13 -8
  170. package/src/sync/sync.ts +2 -0
  171. package/src/util.ts +7 -2
  172. package/src/version.ts +1 -1
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'
3
3
 
4
4
  import { tables } from '../../../__tests__/fixture.ts'
5
5
  import type * as LiveStoreEvent from '../../LiveStoreEvent.ts'
6
- import { ClientDocumentTableDefSymbol, clientDocument } from './client-document-def.ts'
6
+ import { ClientDocumentTableDefSymbol, clientDocument, mergeDefaultValues } from './client-document-def.ts'
7
7
 
8
8
  describe('client document table', () => {
9
9
  test('set event', () => {
@@ -238,3 +238,91 @@ const patchId = (muationEvent: LiveStoreEvent.PartialAnyDecoded) => {
238
238
  const id = `00000000-0000-0000-0000-000000000000`
239
239
  return { ...muationEvent, id }
240
240
  }
241
+
242
+ describe('mergeDefaultValues', () => {
243
+ test('merges values from both objects', () => {
244
+ const defaults = { a: 1, b: 2 }
245
+ const explicit = { a: 10, b: 20 }
246
+ const result = mergeDefaultValues(defaults, explicit)
247
+
248
+ expect(result).toEqual({ a: 10, b: 20 })
249
+ })
250
+
251
+ test('uses default values when explicit values are undefined', () => {
252
+ const defaults = { a: 1, b: 2 }
253
+ const explicit = { a: undefined, b: 20 } as any
254
+ const result = mergeDefaultValues(defaults, explicit)
255
+
256
+ expect(result).toEqual({ a: 1, b: 20 })
257
+ })
258
+
259
+ test('should preserve properties that are not in default values', () => {
260
+ const defaults = { a: 1, b: 2 }
261
+ const explicit = { a: 10, b: 20, c: 30 }
262
+ const result = mergeDefaultValues(defaults, explicit)
263
+
264
+ // Should include ALL properties from explicit, not just those in defaults
265
+ expect(result).toEqual({ a: 10, b: 20, c: 30 })
266
+ expect('c' in result).toBe(true)
267
+ })
268
+
269
+ test('issue #487 - should preserve optional fields not in defaults', () => {
270
+ const defaults = {
271
+ newTodoText: '',
272
+ filter: 'all' as const,
273
+ }
274
+ const userSet = {
275
+ newTodoText: '',
276
+ description: 'First attempt', // Optional field not in defaults
277
+ filter: 'all' as const,
278
+ }
279
+ const result = mergeDefaultValues(defaults, userSet)
280
+
281
+ // Should include the description field even though it's not in defaults
282
+ expect(result).toEqual({
283
+ newTodoText: '',
284
+ description: 'First attempt',
285
+ filter: 'all',
286
+ })
287
+ expect('description' in result).toBe(true)
288
+ })
289
+
290
+ test('handles non-object values', () => {
291
+ expect(mergeDefaultValues('default', 'explicit')).toBe('explicit')
292
+ expect(mergeDefaultValues(42, 100)).toBe(100)
293
+ expect(mergeDefaultValues(null, { a: 1 })).toEqual({ a: 1 })
294
+ expect(mergeDefaultValues({ a: 1 }, null)).toBe(null)
295
+ })
296
+
297
+ test('handles nested objects (current implementation does not deep merge)', () => {
298
+ const defaults = { a: { x: 1, y: 2 }, b: 3 }
299
+ const explicit = { a: { x: 10 }, b: 30 } as any
300
+ const result = mergeDefaultValues(defaults, explicit)
301
+
302
+ // Current implementation replaces entire nested object
303
+ expect(result).toEqual({ a: { x: 10 }, b: 30 })
304
+ // Note: 'y' is lost because the entire 'a' object is replaced
305
+ })
306
+
307
+ test('should handle mix of default and new properties', () => {
308
+ const defaults = {
309
+ required1: 'default1',
310
+ required2: 'default2',
311
+ }
312
+ const userSet = {
313
+ required1: 'user1', // Override default
314
+ required2: 'default2', // Keep default
315
+ optional1: 'new1', // New field
316
+ optional2: 'new2', // New field
317
+ }
318
+ const result = mergeDefaultValues(defaults, userSet)
319
+
320
+ expect(result).toEqual({
321
+ required1: 'user1',
322
+ required2: 'default2',
323
+ optional1: 'new1',
324
+ optional2: 'new2',
325
+ })
326
+ expect(Object.keys(result).sort()).toEqual(['optional1', 'optional2', 'required1', 'required2'])
327
+ })
328
+ })
@@ -17,12 +17,12 @@ import { table } from './table-def.ts'
17
17
  * - Synced across client sessions (e.g. tabs) but not across different clients
18
18
  * - Derived setters
19
19
  * - Emits client-only events
20
- * - Has implicit setter-reducers
20
+ * - Has implicit setter-materializers
21
21
  * - Similar to `React.useState` (except it's persisted)
22
22
  *
23
23
  * Careful:
24
24
  * - When changing the table definitions in a non-backwards compatible way, the state might be lost without
25
- * explicit reducers to handle the old auto-generated events
25
+ * explicit materializers to handle the old auto-generated events
26
26
  *
27
27
  * Usage:
28
28
  *
@@ -121,7 +121,7 @@ export const clientDocument = <
121
121
  return clientDocumentTableDef
122
122
  }
123
123
 
124
- const mergeDefaultValues = <T>(defaultValues: T, explicitDefaultValues: T): T => {
124
+ export const mergeDefaultValues = <T>(defaultValues: T, explicitDefaultValues: T): T => {
125
125
  if (
126
126
  typeof defaultValues !== 'object' ||
127
127
  typeof explicitDefaultValues !== 'object' ||
@@ -131,7 +131,10 @@ const mergeDefaultValues = <T>(defaultValues: T, explicitDefaultValues: T): T =>
131
131
  return explicitDefaultValues
132
132
  }
133
133
 
134
- return Object.keys(defaultValues as any).reduce((acc, key) => {
134
+ // Get all unique keys from both objects
135
+ const allKeys = new Set([...Object.keys(defaultValues as any), ...Object.keys(explicitDefaultValues as any)])
136
+
137
+ return Array.from(allKeys).reduce((acc, key) => {
135
138
  acc[key] = (explicitDefaultValues as any)[key] ?? (defaultValues as any)[key]
136
139
  return acc
137
140
  }, {} as any)
@@ -0,0 +1,212 @@
1
+ import { Schema, SchemaAST } from '@livestore/utils/effect'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ import { withColumnType, withPrimaryKey } from './column-annotations.ts'
5
+
6
+ describe.concurrent('annotations', () => {
7
+ describe('withPrimaryKey', () => {
8
+ test('should add primary key annotation', () => {
9
+ const schema = Schema.String
10
+ const result = withPrimaryKey(schema)
11
+
12
+ expect(SchemaAST.annotations(result.ast, {})).toMatchInlineSnapshot(`
13
+ {
14
+ "_tag": "StringKeyword",
15
+ "annotations": {
16
+ "Symbol(effect/annotation/Description)": "a string",
17
+ "Symbol(effect/annotation/Title)": "string",
18
+ "Symbol(livestore/state/sqlite/annotations/primary-key)": true,
19
+ },
20
+ }
21
+ `)
22
+ })
23
+ })
24
+
25
+ describe('withColumnType', () => {
26
+ describe('compatible schema-column type combinations', () => {
27
+ test('Schema.String with text column type', () => {
28
+ expect(() => withColumnType(Schema.String, 'text')).not.toThrow()
29
+ })
30
+
31
+ test('Schema.Number with integer column type', () => {
32
+ expect(() => withColumnType(Schema.Number, 'integer')).not.toThrow()
33
+ })
34
+
35
+ test('Schema.Number with real column type', () => {
36
+ expect(() => withColumnType(Schema.Number, 'real')).not.toThrow()
37
+ })
38
+
39
+ test('Schema.Boolean with integer column type', () => {
40
+ expect(() => withColumnType(Schema.Boolean, 'integer')).not.toThrow()
41
+ })
42
+
43
+ test('Schema.Uint8ArrayFromSelf with blob column type', () => {
44
+ expect(() => withColumnType(Schema.Uint8ArrayFromSelf, 'blob')).not.toThrow()
45
+ })
46
+
47
+ test('Schema.Date with text column type', () => {
48
+ expect(() => withColumnType(Schema.Date, 'text')).not.toThrow()
49
+ })
50
+
51
+ test('String literal with text column type', () => {
52
+ expect(() => withColumnType(Schema.Literal('hello'), 'text')).not.toThrow()
53
+ })
54
+
55
+ test('Number literal with integer column type', () => {
56
+ expect(() => withColumnType(Schema.Literal(42), 'integer')).not.toThrow()
57
+ })
58
+
59
+ test('Number literal with real column type', () => {
60
+ expect(() => withColumnType(Schema.Literal(3.14), 'real')).not.toThrow()
61
+ })
62
+
63
+ test('Boolean literal with integer column type', () => {
64
+ expect(() => withColumnType(Schema.Literal(true), 'integer')).not.toThrow()
65
+ })
66
+
67
+ test('Union of same type with compatible column type', () => {
68
+ const unionSchema = Schema.Union(Schema.Literal('a'), Schema.Literal('b'))
69
+ expect(() => withColumnType(unionSchema, 'text')).not.toThrow()
70
+ })
71
+
72
+ test('Transformation schema with compatible base type', () => {
73
+ const transformSchema = Schema.transform(Schema.String, Schema.String, {
74
+ decode: (s) => s.toUpperCase(),
75
+ encode: (s) => s.toLowerCase(),
76
+ })
77
+ expect(() => withColumnType(transformSchema, 'text')).not.toThrow()
78
+ })
79
+ })
80
+
81
+ // TODO bring those tests back as we've implemented the column type validation
82
+ // describe('incompatible schema-column type combinations', () => {
83
+ // test('Schema.String with integer column type should throw', () => {
84
+ // expect(() => withColumnType(Schema.String, 'integer')).toThrow(
85
+ // "Schema type 'string' is incompatible with column type 'integer'",
86
+ // )
87
+ // })
88
+
89
+ // test('Schema.String with real column type should throw', () => {
90
+ // expect(() => withColumnType(Schema.String, 'real')).toThrow(
91
+ // "Schema type 'string' is incompatible with column type 'real'",
92
+ // )
93
+ // })
94
+
95
+ // test('Schema.String with blob column type should throw', () => {
96
+ // expect(() => withColumnType(Schema.String, 'blob')).toThrow(
97
+ // "Schema type 'string' is incompatible with column type 'blob'",
98
+ // )
99
+ // })
100
+
101
+ // test('Schema.Number with text column type should throw', () => {
102
+ // expect(() => withColumnType(Schema.Number, 'text')).toThrow(
103
+ // "Schema type 'number' is incompatible with column type 'text'",
104
+ // )
105
+ // })
106
+
107
+ // test('Schema.Number with blob column type should throw', () => {
108
+ // expect(() => withColumnType(Schema.Number, 'blob')).toThrow(
109
+ // "Schema type 'number' is incompatible with column type 'blob'",
110
+ // )
111
+ // })
112
+
113
+ // test('Schema.Boolean with text column type should throw', () => {
114
+ // expect(() => withColumnType(Schema.Boolean, 'text')).toThrow(
115
+ // "Schema type 'boolean' is incompatible with column type 'text'",
116
+ // )
117
+ // })
118
+
119
+ // test('Schema.Boolean with real column type should throw', () => {
120
+ // expect(() => withColumnType(Schema.Boolean, 'real')).toThrow(
121
+ // "Schema type 'boolean' is incompatible with column type 'real'",
122
+ // )
123
+ // })
124
+
125
+ // test('Schema.Boolean with blob column type should throw', () => {
126
+ // expect(() => withColumnType(Schema.Boolean, 'blob')).toThrow(
127
+ // "Schema type 'boolean' is incompatible with column type 'blob'",
128
+ // )
129
+ // })
130
+
131
+ // test('Schema.Uint8ArrayFromSelf with text column type should throw', () => {
132
+ // expect(() => withColumnType(Schema.Uint8ArrayFromSelf, 'text')).toThrow(
133
+ // "Schema type 'uint8array' is incompatible with column type 'text'",
134
+ // )
135
+ // })
136
+
137
+ // test('String literal with integer column type should throw', () => {
138
+ // expect(() => withColumnType(Schema.Literal('hello'), 'integer')).toThrow(
139
+ // "Schema type 'string' is incompatible with column type 'integer'",
140
+ // )
141
+ // })
142
+
143
+ // test('Number literal with text column type should throw', () => {
144
+ // expect(() => withColumnType(Schema.Literal(42), 'text')).toThrow(
145
+ // "Schema type 'number' is incompatible with column type 'text'",
146
+ // )
147
+ // })
148
+
149
+ // test('Schema.Date with integer column type should throw', () => {
150
+ // expect(() => withColumnType(Schema.Date, 'integer')).toThrow(
151
+ // "Schema type 'string' is incompatible with column type 'integer'",
152
+ // )
153
+ // })
154
+ // })
155
+
156
+ describe('complex schemas', () => {
157
+ test('should allow complex schemas that cannot be determined', () => {
158
+ const complexSchema = Schema.Struct({ name: Schema.String, age: Schema.Number })
159
+ expect(() => withColumnType(complexSchema, 'text')).not.toThrow()
160
+ })
161
+
162
+ test('should allow Schema.Any with any column type', () => {
163
+ expect(() => withColumnType(Schema.Any, 'text')).not.toThrow()
164
+ expect(() => withColumnType(Schema.Any, 'integer')).not.toThrow()
165
+ expect(() => withColumnType(Schema.Any, 'real')).not.toThrow()
166
+ expect(() => withColumnType(Schema.Any, 'blob')).not.toThrow()
167
+ })
168
+
169
+ test('should allow Schema.Unknown with any column type', () => {
170
+ expect(() => withColumnType(Schema.Unknown, 'text')).not.toThrow()
171
+ expect(() => withColumnType(Schema.Unknown, 'integer')).not.toThrow()
172
+ expect(() => withColumnType(Schema.Unknown, 'real')).not.toThrow()
173
+ expect(() => withColumnType(Schema.Unknown, 'blob')).not.toThrow()
174
+ })
175
+ })
176
+
177
+ describe('annotation behavior', () => {
178
+ test('should add column type annotation to schema', () => {
179
+ const schema = Schema.String
180
+ const result = withColumnType(schema, 'text')
181
+
182
+ expect(SchemaAST.annotations(result.ast, {})).toMatchInlineSnapshot(`
183
+ {
184
+ "_tag": "StringKeyword",
185
+ "annotations": {
186
+ "Symbol(effect/annotation/Description)": "a string",
187
+ "Symbol(effect/annotation/Title)": "string",
188
+ "Symbol(livestore/state/sqlite/annotations/column-type)": "text",
189
+ },
190
+ }
191
+ `)
192
+ })
193
+
194
+ test('should preserve existing annotations', () => {
195
+ const schema = withPrimaryKey(Schema.String)
196
+ const result = withColumnType(schema, 'text')
197
+
198
+ expect(SchemaAST.annotations(result.ast, {})).toMatchInlineSnapshot(`
199
+ {
200
+ "_tag": "StringKeyword",
201
+ "annotations": {
202
+ "Symbol(effect/annotation/Description)": "a string",
203
+ "Symbol(effect/annotation/Title)": "string",
204
+ "Symbol(livestore/state/sqlite/annotations/column-type)": "text",
205
+ "Symbol(livestore/state/sqlite/annotations/primary-key)": true,
206
+ },
207
+ }
208
+ `)
209
+ })
210
+ })
211
+ })
212
+ })
@@ -0,0 +1,77 @@
1
+ import type { Schema } from '@livestore/utils/effect'
2
+ import { dual } from '@livestore/utils/effect'
3
+ import type { SqliteDsl } from './db-schema/mod.ts'
4
+
5
+ export const PrimaryKeyId = Symbol.for('livestore/state/sqlite/annotations/primary-key')
6
+
7
+ export const ColumnType = Symbol.for('livestore/state/sqlite/annotations/column-type')
8
+
9
+ export const Default = Symbol.for('livestore/state/sqlite/annotations/default')
10
+
11
+ export const AutoIncrement = Symbol.for('livestore/state/sqlite/annotations/auto-increment')
12
+
13
+ export const Unique = Symbol.for('livestore/state/sqlite/annotations/unique')
14
+
15
+ // export const Check = Symbol.for('livestore/state/sqlite/annotations/check')
16
+
17
+ /*
18
+ Here are the knobs you can turn per-column when you CREATE TABLE (or ALTER TABLE … ADD COLUMN) in SQLite:
19
+ • Declared type / affinity – INTEGER, TEXT, REAL, BLOB, NUMERIC, etc. 
20
+ • NULL vs NOT NULL – disallow NULL on inserts/updates. 
21
+ • PRIMARY KEY – makes the column the rowid (and, if the type is INTEGER, it enables rowid-based auto- numbering). Add the optional AUTOINCREMENT keyword if you need monotonic, never-reused ids. 
22
+ • UNIQUE – enforces per-column uniqueness. 
23
+ • DEFAULT <expr> – literal, function (e.g. CURRENT_TIMESTAMP), or parenthesised expression; since 3.46 you can even default to large hex blobs. 
24
+ • CHECK (<expr>) – arbitrary boolean expression evaluated on write. 
25
+ • COLLATE <name> – per-column collation sequence for text comparison. 
26
+ • REFERENCES tbl(col) [ON UPDATE/DELETE …] – column-local foreign key with its own cascade / restrict / set-null rules. 
27
+ • GENERATED ALWAYS AS (<expr>) [VIRTUAL | STORED] – computed columns (since 3.31). 
28
+ • CONSTRAINT name … – optional label in front of any of the above so you can refer to it in error messages or when dropping/recreating schemas.
29
+ */
30
+
31
+ /**
32
+ * Adds a primary key annotation to a schema.
33
+ */
34
+ export const withPrimaryKey = <T extends Schema.Schema.All>(schema: T) =>
35
+ schema.annotations({ [PrimaryKeyId]: true }) as T
36
+
37
+ /**
38
+ * Adds a column type annotation to a schema.
39
+ */
40
+ export const withColumnType: {
41
+ (type: SqliteDsl.FieldColumnType): <T extends Schema.Schema.All>(schema: T) => T
42
+ // TODO make type safe
43
+ <T extends Schema.Schema.All>(schema: T, type: SqliteDsl.FieldColumnType): T
44
+ } = dual(2, <T extends Schema.Schema.All>(schema: T, type: SqliteDsl.FieldColumnType) => {
45
+ validateSchemaColumnTypeCompatibility(schema, type)
46
+ return schema.annotations({ [ColumnType]: type }) as T
47
+ })
48
+
49
+ /**
50
+ * Adds an auto-increment annotation to a schema.
51
+ */
52
+ export const withAutoIncrement = <T extends Schema.Schema.All>(schema: T) =>
53
+ schema.annotations({ [AutoIncrement]: true }) as T
54
+
55
+ /**
56
+ * Adds a unique constraint annotation to a schema.
57
+ */
58
+ export const withUnique = <T extends Schema.Schema.All>(schema: T) => schema.annotations({ [Unique]: true }) as T
59
+
60
+ /**
61
+ * Adds a default value annotation to a schema.
62
+ */
63
+ export const withDefault: {
64
+ // TODO make type safe
65
+ <T extends Schema.Schema.All>(schema: T, value: unknown): T
66
+ (value: unknown): <T extends Schema.Schema.All>(schema: T) => T
67
+ } = dual(2, <T extends Schema.Schema.All>(schema: T, value: unknown) => schema.annotations({ [Default]: value }) as T)
68
+
69
+ /**
70
+ * Validates that a schema is compatible with the specified SQLite column type
71
+ */
72
+ const validateSchemaColumnTypeCompatibility = (
73
+ _schema: Schema.Schema.All,
74
+ _columnType: SqliteDsl.FieldColumnType,
75
+ ): void => {
76
+ // TODO actually implement this
77
+ }