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

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 (162) 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-spec.d.ts +11 -0
  57. package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -0
  58. package/dist/schema/state/sqlite/column-spec.js +39 -0
  59. package/dist/schema/state/sqlite/column-spec.js.map +1 -0
  60. package/dist/schema/state/sqlite/column-spec.test.d.ts +2 -0
  61. package/dist/schema/state/sqlite/column-spec.test.d.ts.map +1 -0
  62. package/dist/schema/state/sqlite/column-spec.test.js +146 -0
  63. package/dist/schema/state/sqlite/column-spec.test.js.map +1 -0
  64. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -0
  65. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  66. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -0
  67. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  68. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +17 -4
  69. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
  70. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -0
  71. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  72. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +65 -165
  73. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  74. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -0
  75. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  76. package/dist/schema/state/sqlite/mod.d.ts +2 -0
  77. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  78. package/dist/schema/state/sqlite/mod.js +2 -0
  79. package/dist/schema/state/sqlite/mod.js.map +1 -1
  80. package/dist/schema/state/sqlite/query-builder/api.d.ts +309 -560
  81. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  82. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts +1 -0
  83. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
  84. package/dist/schema/state/sqlite/query-builder/astToSql.js +8 -6
  85. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
  86. package/dist/schema/state/sqlite/system-tables.d.ts +464 -46
  87. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  88. package/dist/schema/state/sqlite/table-def.d.ts +161 -152
  89. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  90. package/dist/schema/state/sqlite/table-def.js +251 -5
  91. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  92. package/dist/schema/state/sqlite/table-def.test.d.ts +2 -0
  93. package/dist/schema/state/sqlite/table-def.test.d.ts.map +1 -0
  94. package/dist/schema/state/sqlite/table-def.test.js +635 -0
  95. package/dist/schema/state/sqlite/table-def.test.js.map +1 -0
  96. package/dist/schema-management/common.d.ts +1 -1
  97. package/dist/schema-management/common.d.ts.map +1 -1
  98. package/dist/schema-management/common.js +11 -2
  99. package/dist/schema-management/common.js.map +1 -1
  100. package/dist/schema-management/migrations.d.ts +0 -1
  101. package/dist/schema-management/migrations.d.ts.map +1 -1
  102. package/dist/schema-management/migrations.js +4 -30
  103. package/dist/schema-management/migrations.js.map +1 -1
  104. package/dist/schema-management/migrations.test.d.ts +2 -0
  105. package/dist/schema-management/migrations.test.d.ts.map +1 -0
  106. package/dist/schema-management/migrations.test.js +52 -0
  107. package/dist/schema-management/migrations.test.js.map +1 -0
  108. package/dist/sql-queries/types.d.ts +37 -133
  109. package/dist/sqlite-db-helper.d.ts +3 -1
  110. package/dist/sqlite-db-helper.d.ts.map +1 -1
  111. package/dist/sqlite-db-helper.js +16 -0
  112. package/dist/sqlite-db-helper.js.map +1 -1
  113. package/dist/sqlite-types.d.ts +4 -4
  114. package/dist/sqlite-types.d.ts.map +1 -1
  115. package/dist/sync/ClientSessionSyncProcessor.d.ts +2 -2
  116. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  117. package/dist/sync/ClientSessionSyncProcessor.js +8 -7
  118. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  119. package/dist/sync/sync.d.ts.map +1 -1
  120. package/dist/sync/sync.js.map +1 -1
  121. package/dist/util.d.ts +3 -3
  122. package/dist/util.d.ts.map +1 -1
  123. package/dist/util.js.map +1 -1
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/package.json +4 -4
  127. package/src/ClientSessionLeaderThreadProxy.ts +2 -2
  128. package/src/adapter-types.ts +6 -4
  129. package/src/devtools/devtools-messages-leader.ts +3 -3
  130. package/src/leader-thread/LeaderSyncProcessor.ts +3 -1
  131. package/src/leader-thread/make-leader-thread-layer.ts +26 -7
  132. package/src/leader-thread/shutdown-channel.ts +2 -2
  133. package/src/leader-thread/types.ts +1 -1
  134. package/src/materializer-helper.ts +5 -11
  135. package/src/rematerialize-from-eventlog.ts +2 -2
  136. package/src/schema/EventSequenceNumber.test.ts +2 -2
  137. package/src/schema/EventSequenceNumber.ts +8 -2
  138. package/src/schema/LiveStoreEvent.ts +7 -1
  139. package/src/schema/schema.ts +4 -0
  140. package/src/schema/state/sqlite/client-document-def.test.ts +89 -1
  141. package/src/schema/state/sqlite/client-document-def.ts +7 -4
  142. package/src/schema/state/sqlite/column-annotations.test.ts +212 -0
  143. package/src/schema/state/sqlite/column-annotations.ts +77 -0
  144. package/src/schema/state/sqlite/column-spec.test.ts +223 -0
  145. package/src/schema/state/sqlite/column-spec.ts +42 -0
  146. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +2 -0
  147. package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +15 -0
  148. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +20 -2
  149. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +1 -0
  150. package/src/schema/state/sqlite/mod.ts +2 -0
  151. package/src/schema/state/sqlite/query-builder/api.ts +4 -3
  152. package/src/schema/state/sqlite/query-builder/astToSql.ts +9 -7
  153. package/src/schema/state/sqlite/table-def.test.ts +798 -0
  154. package/src/schema/state/sqlite/table-def.ts +472 -16
  155. package/src/schema-management/common.ts +10 -3
  156. package/src/schema-management/migrations.ts +4 -33
  157. package/src/sqlite-db-helper.ts +19 -1
  158. package/src/sqlite-types.ts +4 -4
  159. package/src/sync/ClientSessionSyncProcessor.ts +13 -8
  160. package/src/sync/sync.ts +2 -0
  161. package/src/util.ts +7 -2
  162. package/src/version.ts +1 -1
@@ -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
+ }
@@ -0,0 +1,223 @@
1
+ import { Option, Schema } from '@livestore/utils/effect'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { makeColumnSpec } from './column-spec.ts'
4
+ import { SqliteAst } from './db-schema/mod.ts'
5
+
6
+ const createColumn = (
7
+ name: string,
8
+ type: 'text' | 'integer' | 'real' | 'blob',
9
+ options: {
10
+ nullable?: boolean
11
+ primaryKey?: boolean
12
+ autoIncrement?: boolean
13
+ defaultValue?: unknown
14
+ defaultSql?: string
15
+ } = {},
16
+ ): SqliteAst.Column => {
17
+ let defaultOption: Option.Option<unknown> = Option.none()
18
+ if (options.defaultSql !== undefined) {
19
+ defaultOption = Option.some({ sql: options.defaultSql })
20
+ } else if (options.defaultValue !== undefined) {
21
+ defaultOption = Option.some(options.defaultValue)
22
+ }
23
+
24
+ const schema = (() => {
25
+ switch (type) {
26
+ case 'text':
27
+ return Schema.String
28
+ case 'integer':
29
+ return options.defaultValue === true || options.defaultValue === false ? Schema.Boolean : Schema.Number
30
+ case 'real':
31
+ return Schema.Number
32
+ case 'blob':
33
+ return Schema.Uint8ArrayFromBase64
34
+ default:
35
+ return Schema.Unknown
36
+ }
37
+ })()
38
+
39
+ return SqliteAst.column({
40
+ name,
41
+ type: { _tag: type },
42
+ nullable: options.nullable ?? true,
43
+ primaryKey: options.primaryKey ?? false,
44
+ autoIncrement: options.autoIncrement ?? false,
45
+ default: defaultOption,
46
+ schema,
47
+ })
48
+ }
49
+
50
+ describe('makeColumnSpec', () => {
51
+ it('should quote column names properly for reserved keywords', () => {
52
+ const table = SqliteAst.table(
53
+ 'blocks',
54
+ [createColumn('order', 'integer', { nullable: false }), createColumn('group', 'text')],
55
+ [],
56
+ )
57
+
58
+ const result = makeColumnSpec(table)
59
+ expect(result).toMatchInlineSnapshot(`"'order' integer not null , 'group' text "`)
60
+ expect(result).toContain("'order'")
61
+ expect(result).toContain("'group'")
62
+ })
63
+
64
+ it('should handle basic columns with primary keys', () => {
65
+ const table = SqliteAst.table(
66
+ 'users',
67
+ [createColumn('id', 'text', { nullable: false, primaryKey: true }), createColumn('name', 'text')],
68
+ [],
69
+ )
70
+
71
+ const result = makeColumnSpec(table)
72
+ expect(result).toMatchInlineSnapshot(`"'id' text not null , 'name' text , PRIMARY KEY ('id')"`)
73
+ expect(result).toContain("PRIMARY KEY ('id')")
74
+ })
75
+
76
+ it('should handle multi-column primary keys', () => {
77
+ const table = SqliteAst.table(
78
+ 'composite',
79
+ [
80
+ createColumn('tenant_id', 'text', { nullable: false, primaryKey: true }),
81
+ createColumn('user_id', 'text', { nullable: false, primaryKey: true }),
82
+ ],
83
+ [],
84
+ )
85
+
86
+ const result = makeColumnSpec(table)
87
+ expect(result).toMatchInlineSnapshot(
88
+ `"'tenant_id' text not null , 'user_id' text not null , PRIMARY KEY ('tenant_id', 'user_id')"`,
89
+ )
90
+ expect(result).toContain("PRIMARY KEY ('tenant_id', 'user_id')")
91
+ })
92
+
93
+ it('should handle auto-increment columns', () => {
94
+ const table = SqliteAst.table(
95
+ 'posts',
96
+ [
97
+ createColumn('id', 'integer', { nullable: false, primaryKey: true, autoIncrement: true }),
98
+ createColumn('title', 'text'),
99
+ ],
100
+ [],
101
+ )
102
+
103
+ const result = makeColumnSpec(table)
104
+ expect(result).toMatchInlineSnapshot(`"'id' integer not null autoincrement , 'title' text , PRIMARY KEY ('id')"`)
105
+ expect(result).toContain('autoincrement')
106
+ expect(result).toContain("PRIMARY KEY ('id')")
107
+ })
108
+
109
+ it('should handle columns with default values', () => {
110
+ const table = SqliteAst.table(
111
+ 'products',
112
+ [
113
+ createColumn('id', 'integer', { nullable: false, primaryKey: true }),
114
+ createColumn('name', 'text', { nullable: false }),
115
+ createColumn('price', 'real', { defaultValue: 0 }),
116
+ createColumn('active', 'integer', { defaultValue: true }),
117
+ createColumn('description', 'text', { defaultValue: 'No description' }),
118
+ ],
119
+ [],
120
+ )
121
+
122
+ const result = makeColumnSpec(table)
123
+ expect(result).toMatchInlineSnapshot(
124
+ `"'id' integer not null , 'name' text not null , 'price' real default 0, 'active' integer default true, 'description' text default 'No description', PRIMARY KEY ('id')"`,
125
+ )
126
+ expect(result).toContain('default 0')
127
+ expect(result).toContain('default true')
128
+ expect(result).toContain("default 'No description'")
129
+ })
130
+
131
+ it('should handle columns with SQL default values', () => {
132
+ const table = SqliteAst.table(
133
+ 'logs',
134
+ [
135
+ createColumn('id', 'integer', { nullable: false, primaryKey: true }),
136
+ createColumn('created_at', 'text', { defaultSql: 'CURRENT_TIMESTAMP' }),
137
+ createColumn('random_value', 'real', { defaultSql: 'RANDOM()' }),
138
+ ],
139
+ [],
140
+ )
141
+
142
+ const result = makeColumnSpec(table)
143
+ expect(result).toMatchInlineSnapshot(
144
+ `"'id' integer not null , 'created_at' text default CURRENT_TIMESTAMP, 'random_value' real default RANDOM(), PRIMARY KEY ('id')"`,
145
+ )
146
+ expect(result).toContain('default CURRENT_TIMESTAMP')
147
+ expect(result).toContain('default RANDOM()')
148
+ })
149
+
150
+ it('should handle null default values', () => {
151
+ const table = SqliteAst.table(
152
+ 'nullable_defaults',
153
+ [
154
+ createColumn('id', 'integer', { nullable: false, primaryKey: true }),
155
+ createColumn('optional_text', 'text', { defaultValue: null }),
156
+ ],
157
+ [],
158
+ )
159
+
160
+ const result = makeColumnSpec(table)
161
+ expect(result).toMatchInlineSnapshot(
162
+ `"'id' integer not null , 'optional_text' text default null, PRIMARY KEY ('id')"`,
163
+ )
164
+ expect(result).toContain('default null')
165
+ })
166
+
167
+ it('should handle all column features combined', () => {
168
+ const table = SqliteAst.table(
169
+ 'complex_table',
170
+ [
171
+ createColumn('id', 'integer', {
172
+ nullable: false,
173
+ primaryKey: true,
174
+ autoIncrement: true,
175
+ }),
176
+ createColumn('name', 'text', {
177
+ nullable: false,
178
+ defaultValue: 'Unnamed',
179
+ }),
180
+ createColumn('created_at', 'text', {
181
+ nullable: false,
182
+ defaultSql: 'CURRENT_TIMESTAMP',
183
+ }),
184
+ createColumn('status', 'text', {
185
+ defaultValue: 'pending',
186
+ }),
187
+ ],
188
+ [],
189
+ )
190
+
191
+ const result = makeColumnSpec(table)
192
+ expect(result).toMatchInlineSnapshot(
193
+ `"'id' integer not null autoincrement , 'name' text not null default 'Unnamed', 'created_at' text not null default CURRENT_TIMESTAMP, 'status' text default 'pending', PRIMARY KEY ('id')"`,
194
+ )
195
+ })
196
+
197
+ it('should handle tables with indexes', () => {
198
+ const table = SqliteAst.table(
199
+ 'users_with_indexes',
200
+ [
201
+ createColumn('id', 'integer', { nullable: false, primaryKey: true, autoIncrement: true }),
202
+ createColumn('email', 'text', { nullable: false }),
203
+ createColumn('username', 'text', { nullable: false }),
204
+ createColumn('created_at', 'text', { defaultSql: 'CURRENT_TIMESTAMP' }),
205
+ ],
206
+ [
207
+ SqliteAst.index(['email'], 'idx_users_email', true),
208
+ SqliteAst.index(['username'], 'idx_users_username'),
209
+ SqliteAst.index(['created_at'], 'idx_users_created_at'),
210
+ ],
211
+ )
212
+
213
+ const result = makeColumnSpec(table)
214
+ // The makeColumnSpec function only generates column specifications, not indexes
215
+ expect(result).toMatchInlineSnapshot(
216
+ `"'id' integer not null autoincrement , 'email' text not null , 'username' text not null , 'created_at' text default CURRENT_TIMESTAMP, PRIMARY KEY ('id')"`,
217
+ )
218
+ // Verify the table has the indexes (even though they're not in the column spec)
219
+ expect(table.indexes).toHaveLength(3)
220
+ expect(table.indexes[0]!.unique).toBe(true)
221
+ expect(table.indexes[1]!.unique).toBeUndefined()
222
+ })
223
+ })
@@ -0,0 +1,42 @@
1
+ import { Schema } from '@livestore/utils/effect'
2
+ import { type SqliteAst, SqliteDsl } from './db-schema/mod.ts'
3
+
4
+ /**
5
+ * Returns a SQLite column specification string for a table's column definitions.
6
+ *
7
+ * Example:
8
+ * ```
9
+ * 'id' integer not null autoincrement , 'email' text not null , 'username' text not null , 'created_at' text default CURRENT_TIMESTAMP, PRIMARY KEY ('id')
10
+ * ```
11
+ */
12
+ export const makeColumnSpec = (tableAst: SqliteAst.Table) => {
13
+ const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => `'${_.name}'`)
14
+ const columnDefStrs = tableAst.columns.map(toSqliteColumnSpec)
15
+
16
+ if (primaryKeys.length > 0) {
17
+ columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
18
+ }
19
+
20
+ return columnDefStrs.join(', ')
21
+ }
22
+
23
+ /** NOTE primary keys are applied on a table level not on a column level to account for multi-column primary keys */
24
+ const toSqliteColumnSpec = (column: SqliteAst.Column) => {
25
+ const columnTypeStr = column.type._tag
26
+ const nullableStr = column.nullable === false ? 'not null' : ''
27
+ const autoIncrementStr = column.autoIncrement ? 'autoincrement' : ''
28
+ const defaultValueStr = (() => {
29
+ if (column.default._tag === 'None') return ''
30
+
31
+ if (column.default.value === null) return 'default null'
32
+ if (SqliteDsl.isSqlDefaultValue(column.default.value)) return `default ${column.default.value.sql}`
33
+
34
+ const encodeValue = Schema.encodeSync(column.schema)
35
+ const encodedDefaultValue = encodeValue(column.default.value)
36
+
37
+ if (columnTypeStr === 'text') return `default '${encodedDefaultValue}'`
38
+ return `default ${encodedDefaultValue}`
39
+ })()
40
+
41
+ return `'${column.name}' ${columnTypeStr} ${nullableStr} ${autoIncrementStr} ${defaultValueStr}`
42
+ }
@@ -22,6 +22,7 @@ export type Column = {
22
22
  type: ColumnType.ColumnType
23
23
  primaryKey: boolean
24
24
  nullable: boolean
25
+ autoIncrement: boolean
25
26
  default: Option.Option<any>
26
27
  schema: Schema.Schema<any>
27
28
  }
@@ -106,6 +107,7 @@ const trimInfoForHasing = (obj: Table | Column | Index | ForeignKey | DbSchema):
106
107
  type: obj.type._tag,
107
108
  primaryKey: obj.primaryKey,
108
109
  nullable: obj.nullable,
110
+ autoIncrement: obj.autoIncrement,
109
111
  default: obj.default,
110
112
  }
111
113
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  exports[`FieldDefs > boolean 1`] = `
4
4
  {
5
+ "autoIncrement": false,
5
6
  "columnType": "text",
6
7
  "default": {
7
8
  "_id": "Option",
@@ -15,6 +16,7 @@ exports[`FieldDefs > boolean 1`] = `
15
16
 
16
17
  exports[`FieldDefs > boolean 2`] = `
17
18
  {
19
+ "autoIncrement": false,
18
20
  "columnType": "text",
19
21
  "default": {
20
22
  "_id": "Option",
@@ -28,6 +30,7 @@ exports[`FieldDefs > boolean 2`] = `
28
30
 
29
31
  exports[`FieldDefs > boolean 3`] = `
30
32
  {
33
+ "autoIncrement": false,
31
34
  "columnType": "text",
32
35
  "default": {
33
36
  "_id": "Option",
@@ -42,6 +45,7 @@ exports[`FieldDefs > boolean 3`] = `
42
45
 
43
46
  exports[`FieldDefs > boolean 4`] = `
44
47
  {
48
+ "autoIncrement": false,
45
49
  "columnType": "text",
46
50
  "default": {
47
51
  "_id": "Option",
@@ -56,6 +60,7 @@ exports[`FieldDefs > boolean 4`] = `
56
60
 
57
61
  exports[`FieldDefs > boolean 5`] = `
58
62
  {
63
+ "autoIncrement": false,
59
64
  "columnType": "text",
60
65
  "default": {
61
66
  "_id": "Option",
@@ -70,6 +75,7 @@ exports[`FieldDefs > boolean 5`] = `
70
75
 
71
76
  exports[`FieldDefs > boolean 6`] = `
72
77
  {
78
+ "autoIncrement": false,
73
79
  "columnType": "text",
74
80
  "default": {
75
81
  "_id": "Option",
@@ -83,6 +89,7 @@ exports[`FieldDefs > boolean 6`] = `
83
89
 
84
90
  exports[`FieldDefs > boolean 7`] = `
85
91
  {
92
+ "autoIncrement": false,
86
93
  "columnType": "text",
87
94
  "default": {
88
95
  "_id": "Option",
@@ -97,6 +104,7 @@ exports[`FieldDefs > boolean 7`] = `
97
104
 
98
105
  exports[`FieldDefs > boolean 8`] = `
99
106
  {
107
+ "autoIncrement": false,
100
108
  "columnType": "text",
101
109
  "default": {
102
110
  "_id": "Option",
@@ -113,6 +121,7 @@ exports[`FieldDefs > boolean 8`] = `
113
121
 
114
122
  exports[`FieldDefs > boolean 9`] = `
115
123
  {
124
+ "autoIncrement": false,
116
125
  "columnType": "text",
117
126
  "default": {
118
127
  "_id": "Option",
@@ -126,6 +135,7 @@ exports[`FieldDefs > boolean 9`] = `
126
135
 
127
136
  exports[`FieldDefs > boolean 10`] = `
128
137
  {
138
+ "autoIncrement": false,
129
139
  "columnType": "text",
130
140
  "default": {
131
141
  "_id": "Option",
@@ -139,6 +149,7 @@ exports[`FieldDefs > boolean 10`] = `
139
149
 
140
150
  exports[`FieldDefs > boolean 11`] = `
141
151
  {
152
+ "autoIncrement": false,
142
153
  "columnType": "text",
143
154
  "default": {
144
155
  "_id": "Option",
@@ -153,6 +164,7 @@ exports[`FieldDefs > boolean 11`] = `
153
164
 
154
165
  exports[`FieldDefs > boolean 12`] = `
155
166
  {
167
+ "autoIncrement": false,
156
168
  "columnType": "text",
157
169
  "default": {
158
170
  "_id": "Option",
@@ -167,6 +179,7 @@ exports[`FieldDefs > boolean 12`] = `
167
179
 
168
180
  exports[`FieldDefs > boolean 13`] = `
169
181
  {
182
+ "autoIncrement": false,
170
183
  "columnType": "integer",
171
184
  "default": {
172
185
  "_id": "Option",
@@ -180,6 +193,7 @@ exports[`FieldDefs > boolean 13`] = `
180
193
 
181
194
  exports[`FieldDefs > boolean 14`] = `
182
195
  {
196
+ "autoIncrement": false,
183
197
  "columnType": "integer",
184
198
  "default": {
185
199
  "_id": "Option",
@@ -193,6 +207,7 @@ exports[`FieldDefs > boolean 14`] = `
193
207
 
194
208
  exports[`FieldDefs > boolean 15`] = `
195
209
  {
210
+ "autoIncrement": false,
196
211
  "columnType": "integer",
197
212
  "default": {
198
213
  "_id": "Option",