@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
@@ -1,6 +1,7 @@
1
- import type { Nullable } from '@livestore/utils'
2
- import type { Schema, Types } from '@livestore/utils/effect'
1
+ import { type Nullable, shouldNeverHappen, type Writeable } from '@livestore/utils'
2
+ import { Option, Schema, SchemaAST, type Types } from '@livestore/utils/effect'
3
3
 
4
+ import { AutoIncrement, ColumnType, Default, PrimaryKeyId, Unique } from './column-annotations.ts'
4
5
  import { SqliteDsl } from './db-schema/mod.ts'
5
6
  import type { QueryBuilder } from './query-builder/mod.ts'
6
7
  import { makeQueryBuilder, QueryBuilderAstSymbol, QueryBuilderTypeId } from './query-builder/mod.ts'
@@ -17,6 +18,7 @@ export const TableDefInternalsSymbol = Symbol('TableDefInternals')
17
18
  export type TableDefInternalsSymbol = typeof TableDefInternalsSymbol
18
19
 
19
20
  export type TableDefBase<
21
+ // TODO replace SqliteDef type param with Effect Schema (see below)
20
22
  TSqliteDef extends DefaultSqliteTableDef = DefaultSqliteTableDefConstrained,
21
23
  TOptions extends TableOptions = TableOptions,
22
24
  > = {
@@ -28,10 +30,14 @@ export type TableDefBase<
28
30
  }
29
31
 
30
32
  export type TableDef<
33
+ // TODO replace SqliteDef type param with Effect Schema
34
+ // We can only do this with Effect Schema v4 once the default values are tracked on the type level
35
+ // https://github.com/livestorejs/livestore/issues/382
31
36
  TSqliteDef extends DefaultSqliteTableDef = DefaultSqliteTableDefConstrained,
32
37
  TOptions extends TableOptions = TableOptions,
33
38
  // NOTE we're not using `SqliteDsl.StructSchemaForColumns<TSqliteDef['columns']>`
34
39
  // as we don't want the alias type for users to show up, so we're redefining it here
40
+ // TODO adjust this to `TSchema = Schema.TypeLiteral<` but requires some advance type-level work
35
41
  TSchema = Schema.Schema<
36
42
  SqliteDsl.AnyIfConstained<
37
43
  TSqliteDef['columns'],
@@ -66,28 +72,192 @@ export type TableOptions = {
66
72
  readonly isClientDocumentTable: boolean
67
73
  }
68
74
 
69
- export const table = <
75
+ /**
76
+ * Creates a SQLite table definition from columns or an Effect Schema.
77
+ *
78
+ * This function supports two main ways to define a table:
79
+ * 1. Using explicit column definitions
80
+ * 2. Using an Effect Schema (either the `name` property needs to be provided or the schema needs to have a title/identifier)
81
+ *
82
+ * ```ts
83
+ * // Using explicit columns
84
+ * const usersTable = State.SQLite.table({
85
+ * name: 'users',
86
+ * columns: {
87
+ * id: State.SQLite.text({ primaryKey: true }),
88
+ * name: State.SQLite.text({ nullable: false }),
89
+ * email: State.SQLite.text({ nullable: false }),
90
+ * age: State.SQLite.integer({ nullable: true }),
91
+ * },
92
+ * })
93
+ * ```
94
+ *
95
+ * ```ts
96
+ * // Using Effect Schema with annotations
97
+ * import { Schema } from '@livestore/utils/effect'
98
+ *
99
+ * const UserSchema = Schema.Struct({
100
+ * id: Schema.Int.pipe(State.SQLite.withPrimaryKey).pipe(State.SQLite.withAutoIncrement),
101
+ * email: Schema.String.pipe(State.SQLite.withUnique),
102
+ * name: Schema.String,
103
+ * active: Schema.Boolean.pipe(State.SQLite.withDefault(true)),
104
+ * createdAt: Schema.optional(Schema.Date),
105
+ * })
106
+ *
107
+ * // Option 1: With explicit name
108
+ * const usersTable = State.SQLite.table({
109
+ * name: 'users',
110
+ * schema: UserSchema,
111
+ * })
112
+ *
113
+ * // Option 2: With name from schema annotation (title or identifier)
114
+ * const AnnotatedUserSchema = UserSchema.annotations({ title: 'users' })
115
+ * const usersTable2 = State.SQLite.table({
116
+ * schema: AnnotatedUserSchema,
117
+ * })
118
+ * ```
119
+ *
120
+ * ```ts
121
+ * // Adding indexes
122
+ * const PostSchema = Schema.Struct({
123
+ * id: Schema.String.pipe(State.SQLite.withPrimaryKey),
124
+ * title: Schema.String,
125
+ * authorId: Schema.String,
126
+ * createdAt: Schema.Date,
127
+ * }).annotations({ identifier: 'posts' })
128
+ *
129
+ * const postsTable = State.SQLite.table({
130
+ * schema: PostSchema,
131
+ * indexes: [
132
+ * { name: 'idx_posts_author', columns: ['authorId'] },
133
+ * { name: 'idx_posts_created', columns: ['createdAt'], isUnique: false },
134
+ * ],
135
+ * })
136
+ * ```
137
+ *
138
+ * @remarks
139
+ * - Primary key columns are automatically non-nullable
140
+ * - Columns with `State.SQLite.withUnique` annotation automatically get unique indexes
141
+ * - The `State.SQLite.withAutoIncrement` annotation only works with integer primary keys
142
+ * - Default values can be literal values or SQL expressions
143
+ * - When using Effect Schema without explicit name, the schema must have a title or identifier annotation
144
+ */
145
+ // Overload 1: With columns
146
+ // TODO drop support for `column` when Effect Schema v4 is released
147
+ export function table<
70
148
  TName extends string,
71
- TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition<any, any>,
149
+ TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition.Any,
72
150
  const TOptionsInput extends TableOptionsInput = TableOptionsInput,
73
151
  >(
74
152
  args: {
75
153
  name: TName
76
154
  columns: TColumns
77
155
  } & Partial<TOptionsInput>,
78
- ): TableDef<SqliteTableDefForInput<TName, TColumns>, WithDefaults<TColumns>> => {
79
- const { name, columns: columnOrColumns, ...options } = args
80
- const tablePath = name
156
+ ): TableDef<SqliteTableDefForInput<TName, TColumns>, WithDefaults<TColumns>>
157
+
158
+ // Overload 2: With schema and explicit name
159
+ export function table<
160
+ TName extends string,
161
+ TSchema extends Schema.Schema.AnyNoContext,
162
+ const TOptionsInput extends TableOptionsInput = TableOptionsInput,
163
+ >(
164
+ args: {
165
+ name: TName
166
+ schema: TSchema
167
+ } & Partial<TOptionsInput>,
168
+ ): TableDef<
169
+ SqliteTableDefForSchemaInput<TName, Schema.Schema.Type<TSchema>, Schema.Schema.Encoded<TSchema>, TSchema>,
170
+ TableOptions
171
+ >
172
+
173
+ // Overload 3: With schema and no name (uses schema annotations)
174
+ export function table<
175
+ TSchema extends Schema.Schema.AnyNoContext,
176
+ const TOptionsInput extends TableOptionsInput = TableOptionsInput,
177
+ >(
178
+ args: {
179
+ schema: TSchema
180
+ } & Partial<TOptionsInput>,
181
+ ): TableDef<
182
+ SqliteTableDefForSchemaInput<string, Schema.Schema.Type<TSchema>, Schema.Schema.Encoded<TSchema>, TSchema>,
183
+ TableOptions
184
+ >
185
+
186
+ // Implementation
187
+ export function table<
188
+ TName extends string,
189
+ TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition.Any,
190
+ const TOptionsInput extends TableOptionsInput = TableOptionsInput,
191
+ >(
192
+ args: (
193
+ | {
194
+ name: TName
195
+ columns: TColumns
196
+ }
197
+ | {
198
+ name: TName
199
+ schema: Schema.Schema.AnyNoContext
200
+ }
201
+ | {
202
+ schema: Schema.Schema.AnyNoContext
203
+ }
204
+ ) &
205
+ Partial<TOptionsInput>,
206
+ ): TableDef<any, any> {
207
+ const { ...options } = args
208
+
209
+ let tableName: string
210
+ let columns: SqliteDsl.Columns
211
+ let additionalIndexes: SqliteDsl.Index[] = []
212
+
213
+ if ('columns' in args) {
214
+ tableName = args.name
215
+ const columnOrColumns = args.columns
216
+ columns = (
217
+ SqliteDsl.isColumnDefinition(columnOrColumns) ? { value: columnOrColumns } : columnOrColumns
218
+ ) as SqliteDsl.Columns
219
+ additionalIndexes = []
220
+ } else if ('schema' in args) {
221
+ const result = schemaFieldsToColumns(SchemaAST.getPropertySignatures(args.schema.ast))
222
+ columns = result.columns
223
+
224
+ // We'll set tableName first, then use it for index names
225
+ let tempTableName: string
226
+
227
+ // If name is provided, use it; otherwise extract from schema annotations
228
+ if ('name' in args) {
229
+ tempTableName = args.name
230
+ } else {
231
+ // Use title or identifier, with preference for title
232
+ tempTableName = SchemaAST.getTitleAnnotation(args.schema.ast).pipe(
233
+ Option.orElse(() => SchemaAST.getIdentifierAnnotation(args.schema.ast)),
234
+ Option.getOrElse(() =>
235
+ shouldNeverHappen(
236
+ 'When using schema without explicit name, the schema must have a title or identifier annotation',
237
+ ),
238
+ ),
239
+ )
240
+ }
241
+
242
+ tableName = tempTableName
243
+
244
+ // Create unique indexes for columns with unique annotation
245
+ additionalIndexes = (result.uniqueColumns || []).map((columnName) => ({
246
+ name: `idx_${tableName}_${columnName}_unique`,
247
+ columns: [columnName],
248
+ isUnique: true,
249
+ }))
250
+ } else {
251
+ return shouldNeverHappen('Either `columns` or `schema` must be provided when calling `table()`')
252
+ }
81
253
 
82
254
  const options_: TableOptions = {
83
255
  isClientDocumentTable: false,
84
256
  }
85
257
 
86
- const columns = (
87
- SqliteDsl.isColumnDefinition(columnOrColumns) ? { value: columnOrColumns } : columnOrColumns
88
- ) as SqliteDsl.Columns
89
-
90
- const sqliteDef = SqliteDsl.table(tablePath, columns, options?.indexes ?? [])
258
+ // Combine user-provided indexes with unique column indexes
259
+ const allIndexes = [...(options?.indexes ?? []), ...additionalIndexes]
260
+ const sqliteDef = SqliteDsl.table(tableName, columns, allIndexes)
91
261
 
92
262
  const rowSchema = SqliteDsl.structSchemaForTable(sqliteDef)
93
263
  const insertSchema = SqliteDsl.insertStructSchemaForTable(sqliteDef)
@@ -179,19 +349,305 @@ export namespace FromColumns {
179
349
 
180
350
  export type SqliteTableDefForInput<
181
351
  TName extends string,
182
- TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition<any, any>,
352
+ TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition.Any,
183
353
  > = SqliteDsl.TableDefinition<TName, PrettifyFlat<ToColumns<TColumns>>>
184
354
 
185
- type WithDefaults<TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition<any, any>> = {
355
+ export type SqliteTableDefForSchemaInput<
356
+ TName extends string,
357
+ TType,
358
+ TEncoded,
359
+ _TSchema = any,
360
+ > = TableDefInput.ForSchema<TName, TType, TEncoded, _TSchema>
361
+
362
+ export type WithDefaults<TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition.Any> = {
186
363
  isClientDocumentTable: false
187
364
  requiredInsertColumnNames: SqliteDsl.FromColumns.RequiredInsertColumnNames<ToColumns<TColumns>>
188
365
  }
189
366
 
190
367
  export type PrettifyFlat<T> = T extends infer U ? { [K in keyof U]: U[K] } : never
191
368
 
192
- type ToColumns<TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition<any, any>> =
369
+ export type ToColumns<TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition.Any> =
193
370
  TColumns extends SqliteDsl.Columns
194
371
  ? TColumns
195
- : TColumns extends SqliteDsl.ColumnDefinition<any, any>
372
+ : TColumns extends SqliteDsl.ColumnDefinition.Any
196
373
  ? { value: TColumns }
197
374
  : never
375
+
376
+ export declare namespace SchemaToColumns {
377
+ // Type helper to create column definition with proper schema
378
+ export type ColumnDefForType<TEncoded, TType> = SqliteDsl.ColumnDefinition<TEncoded, TType>
379
+
380
+ // Create columns type from schema Type and Encoded
381
+ export type FromTypes<TType, TEncoded> = TType extends Record<string, any>
382
+ ? TEncoded extends Record<string, any>
383
+ ? {
384
+ [K in keyof TType & keyof TEncoded]: ColumnDefForType<TEncoded[K], TType[K]>
385
+ }
386
+ : SqliteDsl.Columns
387
+ : SqliteDsl.Columns
388
+ }
389
+
390
+ export declare namespace TableDefInput {
391
+ export type ForColumns<
392
+ TName extends string,
393
+ TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition.Any,
394
+ > = SqliteDsl.TableDefinition<TName, PrettifyFlat<ToColumns<TColumns>>>
395
+
396
+ export type ForSchema<TName extends string, TType, TEncoded, _TSchema = any> = SqliteDsl.TableDefinition<
397
+ TName,
398
+ SchemaToColumns.FromTypes<TType, TEncoded>
399
+ >
400
+ }
401
+
402
+ /**
403
+ * Checks if a property signature has a specific annotation, checking both
404
+ * the property signature itself and its type AST.
405
+ */
406
+ const hasPropertyAnnotation = <T>(
407
+ propertySignature: SchemaAST.PropertySignature,
408
+ annotationId: symbol,
409
+ ): Option.Option<T> => {
410
+ // When using Schema.optional(Schema.String).pipe(withPrimaryKey) in a struct,
411
+ // the annotation ends up on a PropertySignatureDeclaration, not the Union type
412
+ // Check if this is a PropertySignatureDeclaration with annotations
413
+ if ('annotations' in propertySignature && propertySignature.annotations) {
414
+ const annotation = SchemaAST.getAnnotation<T>(annotationId)(propertySignature as any)
415
+ if (Option.isSome(annotation)) {
416
+ return annotation
417
+ }
418
+ }
419
+
420
+ // Otherwise check the type AST
421
+ return SchemaAST.getAnnotation<T>(annotationId)(propertySignature.type)
422
+ }
423
+
424
+ /**
425
+ * Maps schema property signatures to SQLite column definitions.
426
+ * Returns both columns and unique column names for index creation.
427
+ */
428
+ const schemaFieldsToColumns = (
429
+ propertySignatures: ReadonlyArray<SchemaAST.PropertySignature>,
430
+ ): { columns: SqliteDsl.Columns; uniqueColumns: string[] } => {
431
+ const columns: SqliteDsl.Columns = {}
432
+ const uniqueColumns: string[] = []
433
+
434
+ for (const prop of propertySignatures) {
435
+ if (typeof prop.name === 'string') {
436
+ // Create a schema from the AST
437
+ const fieldSchema = Schema.make(prop.type)
438
+ // Check if property has primary key annotation
439
+ const hasPrimaryKey = hasPropertyAnnotation<boolean>(prop, PrimaryKeyId).pipe(Option.getOrElse(() => false))
440
+ // Check if property has unique annotation
441
+ const hasUnique = hasPropertyAnnotation<boolean>(prop, Unique).pipe(Option.getOrElse(() => false))
442
+
443
+ columns[prop.name] = schemaFieldToColumn(fieldSchema, prop, hasPrimaryKey)
444
+
445
+ if (hasUnique) {
446
+ uniqueColumns.push(prop.name)
447
+ }
448
+ }
449
+ }
450
+
451
+ return { columns, uniqueColumns }
452
+ }
453
+
454
+ /**
455
+ * Converts a schema field and its property signature to a SQLite column definition.
456
+ */
457
+ const schemaFieldToColumn = (
458
+ fieldSchema: Schema.Schema.AnyNoContext,
459
+ propertySignature: SchemaAST.PropertySignature,
460
+ forceHasPrimaryKey?: boolean,
461
+ ): SqliteDsl.ColumnDefinition.Any => {
462
+ // Determine column type based on schema type
463
+ const columnDef = getColumnDefForSchema(fieldSchema, propertySignature)
464
+
465
+ // Create a new object with appropriate properties
466
+ const result: Writeable<SqliteDsl.ColumnDefinition.Any> = {
467
+ columnType: columnDef.columnType,
468
+ schema: columnDef.schema,
469
+ default: columnDef.default,
470
+ nullable: columnDef.nullable,
471
+ primaryKey: columnDef.primaryKey,
472
+ autoIncrement: columnDef.autoIncrement,
473
+ }
474
+
475
+ // Set primaryKey property explicitly
476
+ if (forceHasPrimaryKey || columnDef.primaryKey) {
477
+ result.primaryKey = true
478
+ } else {
479
+ result.primaryKey = false
480
+ }
481
+
482
+ // Check for invalid primary key + nullable combination
483
+ if (result.primaryKey && (propertySignature.isOptional || columnDef.nullable)) {
484
+ return shouldNeverHappen(
485
+ `Primary key columns cannot be nullable. Found nullable primary key for column. ` +
486
+ `Either remove the primary key annotation or use a non-nullable schema.`,
487
+ )
488
+ }
489
+
490
+ // Set nullable property explicitly
491
+ if (propertySignature.isOptional) {
492
+ result.nullable = true
493
+ } else if (columnDef.nullable) {
494
+ result.nullable = true
495
+ } else {
496
+ result.nullable = false
497
+ }
498
+
499
+ // Only add autoIncrement if it's true
500
+ if (columnDef.autoIncrement) {
501
+ result.autoIncrement = true
502
+ }
503
+
504
+ return result as SqliteDsl.ColumnDefinition.Any
505
+ }
506
+
507
+ /**
508
+ * Maps a schema to a SQLite column definition, respecting column annotations.
509
+ */
510
+ export const getColumnDefForSchema = (
511
+ schema: Schema.Schema.AnyNoContext,
512
+ propertySignature?: SchemaAST.PropertySignature,
513
+ ): SqliteDsl.ColumnDefinition.Any => {
514
+ const ast = schema.ast
515
+
516
+ // Check for annotations
517
+ const hasPrimaryKey = propertySignature
518
+ ? hasPropertyAnnotation<boolean>(propertySignature, PrimaryKeyId).pipe(Option.getOrElse(() => false))
519
+ : SchemaAST.getAnnotation<boolean>(PrimaryKeyId)(ast).pipe(Option.getOrElse(() => false))
520
+
521
+ const hasAutoIncrement = propertySignature
522
+ ? hasPropertyAnnotation<boolean>(propertySignature, AutoIncrement).pipe(Option.getOrElse(() => false))
523
+ : SchemaAST.getAnnotation<boolean>(AutoIncrement)(ast).pipe(Option.getOrElse(() => false))
524
+
525
+ const defaultValue = propertySignature
526
+ ? hasPropertyAnnotation<unknown>(propertySignature, Default)
527
+ : SchemaAST.getAnnotation<unknown>(Default)(ast)
528
+
529
+ /** Adds annotations to a column definition if they are present. */
530
+ const withAnnotationsIfNeeded = (columnDef: SqliteDsl.ColumnDefinition.Any): SqliteDsl.ColumnDefinition.Any => {
531
+ const result = { ...columnDef }
532
+
533
+ if (hasPrimaryKey) {
534
+ result.primaryKey = true
535
+ }
536
+
537
+ if (hasAutoIncrement) {
538
+ result.autoIncrement = true
539
+ }
540
+
541
+ if (Option.isSome(defaultValue)) {
542
+ result.default = Option.some(defaultValue.value)
543
+ }
544
+
545
+ return result
546
+ }
547
+
548
+ // Check for custom column type annotation
549
+ const columnTypeAnnotation = SchemaAST.getAnnotation<SqliteDsl.FieldColumnType>(ColumnType)(ast)
550
+ if (Option.isSome(columnTypeAnnotation)) {
551
+ const columnType = columnTypeAnnotation.value
552
+ let columnDef: SqliteDsl.ColumnDefinition.Any
553
+ switch (columnType) {
554
+ case 'text':
555
+ columnDef = SqliteDsl.text()
556
+ break
557
+ case 'integer':
558
+ columnDef = SqliteDsl.integer()
559
+ break
560
+ case 'real':
561
+ columnDef = SqliteDsl.real()
562
+ break
563
+ case 'blob':
564
+ columnDef = SqliteDsl.blob()
565
+ break
566
+ default:
567
+ return shouldNeverHappen(`Unsupported column type annotation: ${columnType}`)
568
+ }
569
+
570
+ return withAnnotationsIfNeeded(columnDef)
571
+ }
572
+
573
+ // Check for refinements (e.g., Schema.Int)
574
+ if (SchemaAST.isRefinement(ast)) {
575
+ // Check if this is specifically Schema.Int by looking at the identifier annotation
576
+ const identifier = SchemaAST.getIdentifierAnnotation(ast).pipe(Option.getOrElse(() => ''))
577
+ if (identifier === 'Int') {
578
+ return withAnnotationsIfNeeded(SqliteDsl.integer())
579
+ }
580
+ // For other refinements, check the underlying type
581
+ return getColumnDefForSchema(Schema.make(ast.from), propertySignature)
582
+ }
583
+
584
+ // Check for string types
585
+ if (SchemaAST.isStringKeyword(ast)) {
586
+ return withAnnotationsIfNeeded(SqliteDsl.text())
587
+ }
588
+
589
+ // Check for number types
590
+ if (SchemaAST.isNumberKeyword(ast)) {
591
+ return withAnnotationsIfNeeded(SqliteDsl.real())
592
+ }
593
+
594
+ // Check for boolean types
595
+ if (SchemaAST.isBooleanKeyword(ast)) {
596
+ return withAnnotationsIfNeeded(SqliteDsl.boolean())
597
+ }
598
+
599
+ // Check for unions (like optional or nullable)
600
+ if (SchemaAST.isUnion(ast)) {
601
+ // Check if this union contains null or undefined (making it nullable/optional)
602
+ let hasNull = false
603
+ let hasUndefined = false
604
+ let nonNullableType: SchemaAST.AST | undefined
605
+
606
+ for (const type of ast.types) {
607
+ if (SchemaAST.isUndefinedKeyword(type)) {
608
+ hasUndefined = true
609
+ } else if (SchemaAST.isLiteral(type) && type.literal === null) {
610
+ hasNull = true
611
+ } else {
612
+ nonNullableType = type
613
+ }
614
+ }
615
+
616
+ // If we found a non-nullable type, use it for the column definition
617
+ if (nonNullableType) {
618
+ const innerSchema = Schema.make(nonNullableType)
619
+ const innerColumnDef = getColumnDefForSchema(innerSchema, propertySignature)
620
+
621
+ // If the union contains null or undefined, mark as nullable
622
+ if (hasNull || hasUndefined) {
623
+ return withAnnotationsIfNeeded({
624
+ ...innerColumnDef,
625
+ nullable: true,
626
+ })
627
+ }
628
+
629
+ return withAnnotationsIfNeeded(innerColumnDef)
630
+ }
631
+ }
632
+
633
+ // Check for Date types
634
+ if (SchemaAST.isTransformation(ast)) {
635
+ // Try to map the transformation's target type
636
+ return getColumnDefForSchema(Schema.make(ast.to), propertySignature)
637
+ }
638
+
639
+ // Check for literal types
640
+ if (SchemaAST.isLiteral(ast)) {
641
+ const value = ast.literal
642
+ if (typeof value === 'string') {
643
+ return withAnnotationsIfNeeded(SqliteDsl.text())
644
+ } else if (typeof value === 'number') {
645
+ return withAnnotationsIfNeeded(SqliteDsl.real())
646
+ } else if (typeof value === 'boolean') {
647
+ return withAnnotationsIfNeeded(SqliteDsl.boolean())
648
+ }
649
+ }
650
+
651
+ // Default to JSON column for complex types
652
+ return withAnnotationsIfNeeded(SqliteDsl.json({ schema }))
653
+ }
@@ -1,4 +1,4 @@
1
- import type { SqliteDb } from '../adapter-types.ts'
1
+ import { type SqliteDb, SqliteError } from '../adapter-types.ts'
2
2
  import type { ParamsObject } from '../util.ts'
3
3
  import { prepareBindValues } from '../util.ts'
4
4
 
@@ -15,9 +15,16 @@ export const dbExecute = (db: SqliteDb, queryStr: string, bindValues?: ParamsObj
15
15
 
16
16
  const preparedBindValues = bindValues ? prepareBindValues(bindValues, queryStr) : undefined
17
17
 
18
- stmt.execute(preparedBindValues)
18
+ try {
19
+ stmt.execute(preparedBindValues)
19
20
 
20
- stmt.finalize()
21
+ stmt.finalize()
22
+ } catch (cause) {
23
+ throw new SqliteError({
24
+ cause,
25
+ query: { sql: queryStr, bindValues: preparedBindValues ?? {} },
26
+ })
27
+ }
21
28
  }
22
29
 
23
30
  export const dbSelect = <T>(db: SqliteDb, queryStr: string, bindValues?: ParamsObject) => {
@@ -1,11 +1,12 @@
1
1
  import { memoizeByStringifyArgs } from '@livestore/utils'
2
- import { Effect, Schema as EffectSchema } from '@livestore/utils/effect'
2
+ import { Effect } from '@livestore/utils/effect'
3
3
 
4
4
  import type { SqliteDb } from '../adapter-types.ts'
5
5
  import type { MigrationsReport, MigrationsReportEntry } from '../defs.ts'
6
6
  import type { UnexpectedError } from '../errors.ts'
7
7
  import type { LiveStoreSchema } from '../schema/mod.ts'
8
- import { SqliteAst, SqliteDsl } from '../schema/state/sqlite/db-schema/mod.ts'
8
+ import { makeColumnSpec } from '../schema/state/sqlite/column-spec.ts'
9
+ import { SqliteAst } from '../schema/state/sqlite/db-schema/mod.ts'
9
10
  import type { SchemaEventDefsMetaRow, SchemaMetaRow } from '../schema/state/sqlite/system-tables.ts'
10
11
  import {
11
12
  isStateSystemTable,
@@ -169,35 +170,5 @@ export const migrateTable = ({
169
170
 
170
171
  const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
171
172
  const uniqueStr = index.unique ? 'UNIQUE' : ''
172
- return sql`create ${uniqueStr} index if not exists '${index.name}' on '${tableName}' (${index.columns.join(', ')})`
173
- }
174
-
175
- export const makeColumnSpec = (tableAst: SqliteAst.Table) => {
176
- const primaryKeys = tableAst.columns.filter((_) => _.primaryKey).map((_) => `'${_.name}'`)
177
- const columnDefStrs = tableAst.columns.map(toSqliteColumnSpec)
178
- if (primaryKeys.length > 0) {
179
- columnDefStrs.push(`PRIMARY KEY (${primaryKeys.join(', ')})`)
180
- }
181
-
182
- return columnDefStrs.join(', ')
183
- }
184
-
185
- /** NOTE primary keys are applied on a table level not on a column level to account for multi-column primary keys */
186
- const toSqliteColumnSpec = (column: SqliteAst.Column) => {
187
- const columnTypeStr = column.type._tag
188
- const nullableStr = column.nullable === false ? 'not null' : ''
189
- const defaultValueStr = (() => {
190
- if (column.default._tag === 'None') return ''
191
-
192
- if (column.default.value === null) return 'default null'
193
- if (SqliteDsl.isSqlDefaultValue(column.default.value)) return `default ${column.default.value.sql}`
194
-
195
- const encodeValue = EffectSchema.encodeSync(column.schema)
196
- const encodedDefaultValue = encodeValue(column.default.value)
197
-
198
- if (columnTypeStr === 'text') return `default '${encodedDefaultValue}'`
199
- return `default ${encodedDefaultValue}`
200
- })()
201
-
202
- return `'${column.name}' ${columnTypeStr} ${nullableStr} ${defaultValueStr}`
173
+ return sql`create ${uniqueStr} index if not exists '${index.name}' on '${tableName}' (${index.columns.map((col) => `'${col}'`).join(', ')})`
203
174
  }
@@ -1,6 +1,6 @@
1
1
  import { Schema } from '@livestore/utils/effect'
2
2
 
3
- import type { SqliteDb } from './adapter-types.ts'
3
+ import { type SqliteDb, SqliteError } from './adapter-types.ts'
4
4
  import { getResultSchema, isQueryBuilder } from './schema/state/sqlite/query-builder/mod.ts'
5
5
  import type { PreparedBindValues } from './util.ts'
6
6
 
@@ -39,3 +39,21 @@ export const makeSelect = <T>(
39
39
  }
40
40
  }
41
41
  }
42
+
43
+ export const validateSnapshot = (snapshot: Uint8Array) => {
44
+ const headerBytes = new TextDecoder().decode(snapshot.slice(0, 16))
45
+ const hasValidHeader = headerBytes.startsWith('SQLite format 3')
46
+
47
+ if (!hasValidHeader) {
48
+ throw new SqliteError({
49
+ cause: 'Invalid SQLite header',
50
+ note: `Expected header to start with 'SQLite format 3', but got: ${headerBytes}`,
51
+ })
52
+ }
53
+ }
54
+
55
+ export const makeExport = (exportFn: () => Uint8Array<ArrayBuffer>) => () => {
56
+ const snapshot = exportFn()
57
+ validateSnapshot(snapshot)
58
+ return snapshot
59
+ }
@@ -25,12 +25,12 @@ export interface SqliteDb<TReq = any, TMetadata extends TReq = TReq> {
25
25
  select<T>(queryStr: string, bindValues?: PreparedBindValues | undefined): ReadonlyArray<T>
26
26
  select<T>(queryBuilder: QueryBuilder<T, any, any>): T
27
27
 
28
- export(): Uint8Array
29
- import: (data: Uint8Array | SqliteDb<TReq>) => void
28
+ export(): Uint8Array<ArrayBuffer>
29
+ import: (data: Uint8Array<ArrayBuffer> | SqliteDb<TReq>) => void
30
30
  close(): void
31
31
  destroy(): void
32
32
  session(): SqliteDbSession
33
- makeChangeset: (data: Uint8Array) => SqliteDbChangeset
33
+ makeChangeset: (data: Uint8Array<ArrayBuffer>) => SqliteDbChangeset
34
34
  }
35
35
 
36
36
  export type SqliteDebugInfo = { head: EventSequenceNumber.EventSequenceNumber }
@@ -56,7 +56,7 @@ export interface PreparedStatement {
56
56
  }
57
57
 
58
58
  export type SqliteDbSession = {
59
- changeset: () => Uint8Array | undefined
59
+ changeset: () => Uint8Array<ArrayBuffer> | undefined
60
60
  finish: () => void
61
61
  }
62
62