@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
@@ -0,0 +1,290 @@
1
+ import { shouldNeverHappen, type Writeable } from '@livestore/utils'
2
+ import { Option, Schema, SchemaAST } from '@livestore/utils/effect'
3
+
4
+ import { AutoIncrement, ColumnType, Default, PrimaryKeyId, Unique } from './column-annotations.ts'
5
+ import { SqliteDsl } from './db-schema/mod.ts'
6
+
7
+ /**
8
+ * Maps a schema to a SQLite column definition, respecting column annotations.
9
+ */
10
+ export const getColumnDefForSchema = (
11
+ schema: Schema.Schema.AnyNoContext,
12
+ propertySignature?: SchemaAST.PropertySignature,
13
+ ): SqliteDsl.ColumnDefinition.Any => {
14
+ const ast = schema.ast
15
+
16
+ // 1. Extract annotations
17
+ const getAnnotation = <T>(annotationId: symbol): Option.Option<T> =>
18
+ propertySignature
19
+ ? hasPropertyAnnotation<T>(propertySignature, annotationId)
20
+ : SchemaAST.getAnnotation<T>(annotationId)(ast)
21
+
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
+ }
28
+
29
+ // 2. Resolve the core type and nullable info
30
+ const typeInfo = resolveType(ast)
31
+
32
+ // 3. Create column definition based on resolved type
33
+ let columnDef: SqliteDsl.ColumnDefinition.Any
34
+
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
+ }
62
+
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)
69
+ }
70
+
71
+ return result
72
+ }
73
+
74
+ /**
75
+ * Checks if a property signature has a specific annotation, checking both
76
+ * the property signature itself and its type AST.
77
+ */
78
+ const hasPropertyAnnotation = <T>(
79
+ propertySignature: SchemaAST.PropertySignature,
80
+ annotationId: symbol,
81
+ ): 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
+ if ('annotations' in propertySignature && propertySignature.annotations) {
86
+ const annotation = SchemaAST.getAnnotation<T>(annotationId)(propertySignature as any)
87
+ if (Option.isSome(annotation)) {
88
+ return annotation
89
+ }
90
+ }
91
+
92
+ // Otherwise check the type AST
93
+ return SchemaAST.getAnnotation<T>(annotationId)(propertySignature.type)
94
+ }
95
+
96
+ /**
97
+ * Maps schema property signatures to SQLite column definitions.
98
+ * Returns both columns and unique column names for index creation.
99
+ */
100
+ export const schemaFieldsToColumns = (
101
+ propertySignatures: ReadonlyArray<SchemaAST.PropertySignature>,
102
+ ): { columns: SqliteDsl.Columns; uniqueColumns: string[] } => {
103
+ const columns: SqliteDsl.Columns = {}
104
+ const uniqueColumns: string[] = []
105
+
106
+ 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)
119
+ }
120
+ }
121
+ }
122
+
123
+ return { columns, uniqueColumns }
124
+ }
125
+
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
+ }
146
+
147
+ // Set primaryKey property explicitly
148
+ if (forceHasPrimaryKey || columnDef.primaryKey) {
149
+ result.primaryKey = true
150
+ } else {
151
+ result.primaryKey = false
152
+ }
153
+
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
+ }
161
+
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
+ }
170
+
171
+ // Only add autoIncrement if it's true
172
+ if (columnDef.autoIncrement) {
173
+ result.autoIncrement = true
174
+ }
175
+
176
+ return result as SqliteDsl.ColumnDefinition.Any
177
+ }
178
+
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
+
193
+ let hasNull = false
194
+ let hasUndefined = false
195
+ let coreType: SchemaAST.AST | undefined
196
+
197
+ 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
+ }
207
+ }
208
+
209
+ ast.types.forEach(visit)
210
+ return { coreType: coreType || ast, hasNull, hasUndefined }
211
+ }
212
+
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))
230
+ }
231
+
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()
243
+ }
244
+
245
+ // Everything else is complex
246
+ return SqliteDsl.json({ schema })
247
+ }
248
+
249
+ /**
250
+ * Creates a column from a specific column type string.
251
+ */
252
+ const createColumnFromType = (columnType: string, ast: SchemaAST.AST): SqliteDsl.ColumnDefinition.Any => {
253
+ switch (columnType) {
254
+ case 'text':
255
+ return SqliteDsl.text()
256
+ case 'integer':
257
+ // Preserve boolean transformation
258
+ return SchemaAST.isBooleanKeyword(ast) ? SqliteDsl.boolean() : SqliteDsl.integer()
259
+ case 'real':
260
+ return SqliteDsl.real()
261
+ case 'blob':
262
+ return SqliteDsl.blob()
263
+ default:
264
+ return shouldNeverHappen(`Unsupported column type: ${columnType}`)
265
+ }
266
+ }
267
+
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
279
+ }
280
+
281
+ if (SchemaAST.isRefinement(ast)) {
282
+ return isPrimitiveAST(ast.from)
283
+ }
284
+
285
+ if (SchemaAST.isTransformation(ast)) {
286
+ return isPrimitiveAST(ast.to)
287
+ }
288
+
289
+ return false
290
+ }
@@ -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",