@livestore/common 0.0.47-dev.0 → 0.0.48-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 (108) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +68 -0
  3. package/dist/__tests__/fixture.d.ts.map +1 -0
  4. package/dist/__tests__/fixture.js +16 -0
  5. package/dist/__tests__/fixture.js.map +1 -0
  6. package/dist/adapter-types.d.ts +86 -0
  7. package/dist/adapter-types.d.ts.map +1 -0
  8. package/dist/adapter-types.js +2 -0
  9. package/dist/adapter-types.js.map +1 -0
  10. package/dist/derived-mutations.d.ts +107 -0
  11. package/dist/derived-mutations.d.ts.map +1 -0
  12. package/dist/derived-mutations.js +51 -0
  13. package/dist/derived-mutations.js.map +1 -0
  14. package/dist/derived-mutations.test.d.ts +2 -0
  15. package/dist/derived-mutations.test.d.ts.map +1 -0
  16. package/dist/derived-mutations.test.js +92 -0
  17. package/dist/derived-mutations.test.js.map +1 -0
  18. package/dist/index.d.ts +6 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/init-singleton-tables.d.ts +2 -2
  23. package/dist/init-singleton-tables.d.ts.map +1 -1
  24. package/dist/init-singleton-tables.js.map +1 -1
  25. package/dist/mutation.d.ts.map +1 -1
  26. package/dist/query-info.d.ts +47 -0
  27. package/dist/query-info.d.ts.map +1 -0
  28. package/dist/query-info.js +38 -0
  29. package/dist/query-info.js.map +1 -0
  30. package/dist/rehydrate-from-mutationlog.d.ts +6 -5
  31. package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
  32. package/dist/rehydrate-from-mutationlog.js +24 -7
  33. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  34. package/dist/schema/index.d.ts +33 -23
  35. package/dist/schema/index.d.ts.map +1 -1
  36. package/dist/schema/index.js +31 -15
  37. package/dist/schema/index.js.map +1 -1
  38. package/dist/schema/mutations.d.ts +16 -0
  39. package/dist/schema/mutations.d.ts.map +1 -1
  40. package/dist/schema/mutations.js +18 -8
  41. package/dist/schema/mutations.js.map +1 -1
  42. package/dist/schema/parse-utils.d.ts +14 -4
  43. package/dist/schema/parse-utils.d.ts.map +1 -1
  44. package/dist/schema/parse-utils.js +3 -3
  45. package/dist/schema/parse-utils.js.map +1 -1
  46. package/dist/schema/system-tables.d.ts +239 -67
  47. package/dist/schema/system-tables.d.ts.map +1 -1
  48. package/dist/schema/system-tables.js +24 -3
  49. package/dist/schema/system-tables.js.map +1 -1
  50. package/dist/schema/table-def.d.ts +53 -10
  51. package/dist/schema/table-def.d.ts.map +1 -1
  52. package/dist/schema/table-def.js +27 -12
  53. package/dist/schema/table-def.js.map +1 -1
  54. package/dist/schema-management/common.d.ts +5 -0
  55. package/dist/schema-management/common.d.ts.map +1 -0
  56. package/dist/schema-management/common.js +22 -0
  57. package/dist/schema-management/common.js.map +1 -0
  58. package/dist/schema-management/migrations.d.ts +18 -0
  59. package/dist/schema-management/migrations.d.ts.map +1 -0
  60. package/dist/{migrations.js → schema-management/migrations.js} +29 -36
  61. package/dist/schema-management/migrations.js.map +1 -0
  62. package/dist/schema-management/validate-mutation-defs.d.ts +16 -0
  63. package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -0
  64. package/dist/schema-management/validate-mutation-defs.js +63 -0
  65. package/dist/schema-management/validate-mutation-defs.js.map +1 -0
  66. package/dist/sql-queries/sql-queries.d.ts +4 -4
  67. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  68. package/dist/sql-queries/sql-queries.js +7 -8
  69. package/dist/sql-queries/sql-queries.js.map +1 -1
  70. package/dist/sql-queries/sql-query-builder.d.ts +1 -1
  71. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  72. package/dist/sync/index.d.ts +2 -0
  73. package/dist/sync/index.d.ts.map +1 -0
  74. package/dist/sync/index.js +2 -0
  75. package/dist/sync/index.js.map +1 -0
  76. package/dist/sync/sync.d.ts +25 -0
  77. package/dist/sync/sync.d.ts.map +1 -0
  78. package/dist/sync/sync.js +8 -0
  79. package/dist/sync/sync.js.map +1 -0
  80. package/package.json +6 -6
  81. package/src/__tests__/fixture.ts +23 -0
  82. package/src/adapter-types.ts +104 -0
  83. package/src/ambient.d.ts +3 -0
  84. package/src/derived-mutations.test.ts +100 -0
  85. package/src/derived-mutations.ts +126 -0
  86. package/src/index.ts +6 -2
  87. package/src/init-singleton-tables.ts +2 -2
  88. package/src/query-info.ts +104 -0
  89. package/src/rehydrate-from-mutationlog.ts +34 -20
  90. package/src/schema/index.ts +67 -39
  91. package/src/schema/mutations.ts +28 -9
  92. package/src/schema/parse-utils.ts +3 -3
  93. package/src/schema/system-tables.ts +44 -3
  94. package/src/schema/table-def.ts +64 -18
  95. package/src/schema-management/common.ts +29 -0
  96. package/src/{migrations.ts → schema-management/migrations.ts} +44 -57
  97. package/src/schema-management/validate-mutation-defs.ts +108 -0
  98. package/src/sql-queries/sql-queries.ts +8 -9
  99. package/src/sync/index.ts +1 -0
  100. package/src/sync/sync.ts +14 -0
  101. package/dist/database.d.ts +0 -50
  102. package/dist/database.d.ts.map +0 -1
  103. package/dist/database.js +0 -2
  104. package/dist/database.js.map +0 -1
  105. package/dist/migrations.d.ts +0 -16
  106. package/dist/migrations.d.ts.map +0 -1
  107. package/dist/migrations.js.map +0 -1
  108. package/src/database.ts +0 -60
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { appConfig, todos } from './__tests__/fixture.js'
4
+ import type { MutationEvent } from './schema/mutations.js'
5
+
6
+ describe('derived mutations', () => {
7
+ test('todos', () => {
8
+ expect(patchId(todos.insert({ id: 't1', completed: true, text: 'Task 1' }))).toMatchInlineSnapshot(`
9
+ {
10
+ "args": {
11
+ "completed": true,
12
+ "id": "t1",
13
+ "text": "Task 1",
14
+ },
15
+ "id": "00000000-0000-0000-0000-000000000000",
16
+ "mutation": "_Derived_Create_todos",
17
+ }
18
+ `)
19
+
20
+ expect(patchId(todos.update({ where: { id: 't1' }, values: { text: 'Task 1 - fixed' } }))).toMatchInlineSnapshot(`
21
+ {
22
+ "args": {
23
+ "values": {
24
+ "text": "Task 1 - fixed",
25
+ },
26
+ "where": {
27
+ "id": "t1",
28
+ },
29
+ },
30
+ "id": "00000000-0000-0000-0000-000000000000",
31
+ "mutation": "_Derived_Update_todos",
32
+ }
33
+ `)
34
+
35
+ expect(patchId(todos.delete({ where: { id: 't1' } }))).toMatchInlineSnapshot(`
36
+ {
37
+ "args": {
38
+ "where": {
39
+ "id": "t1",
40
+ },
41
+ },
42
+ "id": "00000000-0000-0000-0000-000000000000",
43
+ "mutation": "_Derived_Delete_todos",
44
+ }
45
+ `)
46
+ })
47
+
48
+ test('app_config', () => {
49
+ expect(patchId(appConfig.insert())).toMatchInlineSnapshot(`
50
+ {
51
+ "args": {
52
+ "id": "singleton",
53
+ "value": {
54
+ "value": undefined,
55
+ },
56
+ },
57
+ "id": "00000000-0000-0000-0000-000000000000",
58
+ "mutation": "_Derived_Create_app_config",
59
+ }
60
+ `)
61
+
62
+ expect(patchId(appConfig.insert({ fontSize: 12, theme: 'dark' }))).toMatchInlineSnapshot(`
63
+ {
64
+ "args": {
65
+ "id": "singleton",
66
+ "value": {
67
+ "value": {
68
+ "fontSize": 12,
69
+ "theme": "dark",
70
+ },
71
+ },
72
+ },
73
+ "id": "00000000-0000-0000-0000-000000000000",
74
+ "mutation": "_Derived_Create_app_config",
75
+ }
76
+ `)
77
+
78
+ expect(patchId(appConfig.update({ fontSize: 13 }))).toMatchInlineSnapshot(`
79
+ {
80
+ "args": {
81
+ "values": {
82
+ "value": {
83
+ "fontSize": 13,
84
+ },
85
+ },
86
+ "where": {
87
+ "id": "singleton",
88
+ },
89
+ },
90
+ "id": "00000000-0000-0000-0000-000000000000",
91
+ "mutation": "_Derived_Update_app_config",
92
+ }
93
+ `)
94
+ })
95
+ })
96
+
97
+ const patchId = (muationEvent: MutationEvent.Any) => {
98
+ const id = `00000000-0000-0000-0000-000000000000`
99
+ return { ...muationEvent, id }
100
+ }
@@ -0,0 +1,126 @@
1
+ import type { GetValForKey } from '@livestore/utils'
2
+ import { ReadonlyRecord, Schema } from '@livestore/utils/effect'
3
+ import type { SqliteDsl } from 'effect-db-schema'
4
+
5
+ import type { MutationEvent } from './schema/index.js'
6
+ import { DbSchema, defineMutation } from './schema/index.js'
7
+ import type { TableOptions } from './schema/table-def.js'
8
+ import { deleteRows, insertRow, updateRows } from './sql-queries/sql-queries.js'
9
+
10
+ export const makeDerivedMutationDefsForTable = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => ({
11
+ insert: deriveCreateMutationDef(table),
12
+ update: deriveUpdateMutationDef(table),
13
+ delete: deriveDeleteMutationDef(table),
14
+ })
15
+
16
+ export const deriveCreateMutationDef = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => {
17
+ const tableName = table.sqliteDef.name
18
+
19
+ const [optionalFields, requiredColumns] = ReadonlyRecord.partition(
20
+ (table.sqliteDef as DbSchema.DefaultSqliteTableDef).columns,
21
+ (col) => col.nullable === false && col.default._tag === 'None',
22
+ )
23
+
24
+ const insertSchema = Schema.Struct(ReadonlyRecord.map(requiredColumns, (col) => col.schema)).pipe(
25
+ Schema.extend(Schema.partial(Schema.Struct(ReadonlyRecord.map(optionalFields, (col) => col.schema)))),
26
+ )
27
+
28
+ return defineMutation(`_Derived_Create_${tableName}`, insertSchema, ({ id, ...explicitDefaultValues }) => {
29
+ const defaultValues = DbSchema.getDefaultValuesDecoded(table, explicitDefaultValues)
30
+
31
+ const [sql, bindValues] = insertRow({
32
+ tableName: table.sqliteDef.name,
33
+ columns: table.sqliteDef.columns,
34
+ values: { ...defaultValues, id },
35
+ })
36
+
37
+ return { sql, bindValues, writeTables: new Set([tableName]) }
38
+ })
39
+ }
40
+
41
+ export const deriveUpdateMutationDef = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => {
42
+ const tableName = table.sqliteDef.name
43
+
44
+ return defineMutation(
45
+ `_Derived_Update_${tableName}`,
46
+ Schema.Struct({
47
+ where: Schema.partial(table.schema),
48
+ values: Schema.partial(table.schema),
49
+ }),
50
+ ({ where, values }) => {
51
+ const [sql, bindValues] = updateRows({
52
+ tableName: table.sqliteDef.name,
53
+ columns: table.sqliteDef.columns,
54
+ where,
55
+ updateValues: values,
56
+ })
57
+
58
+ return { sql, bindValues, writeTables: new Set([tableName]) }
59
+ },
60
+ )
61
+ }
62
+
63
+ export const deriveDeleteMutationDef = <TTableDef extends DbSchema.TableDef>(table: TTableDef) => {
64
+ const tableName = table.sqliteDef.name
65
+
66
+ return defineMutation(
67
+ `_Derived_Delete_${tableName}`,
68
+ Schema.Struct({
69
+ where: Schema.partial(table.schema),
70
+ }),
71
+ ({ where }) => {
72
+ const [sql, bindValues] = deleteRows({
73
+ tableName: table.sqliteDef.name,
74
+ columns: table.sqliteDef.columns,
75
+ where,
76
+ })
77
+
78
+ return { sql, bindValues, writeTables: new Set([tableName]) }
79
+ },
80
+ )
81
+ }
82
+
83
+ /**
84
+ * Convenience helper functions on top of the derived mutation definitions.
85
+ */
86
+ export type DerivedMutationHelperFns<TColumns extends SqliteDsl.ConstraintColumns, TOptions extends TableOptions> = {
87
+ insert: DerivedMutationHelperFns.InsertMutationFn<TColumns, TOptions>
88
+ update: DerivedMutationHelperFns.UpdateMutationFn<TColumns, TOptions>
89
+ delete: DerivedMutationHelperFns.DeleteMutationFn<TColumns, TOptions>
90
+ // TODO also consider adding upsert and deep json mutations (like lenses)
91
+ }
92
+
93
+ export namespace DerivedMutationHelperFns {
94
+ export type InsertMutationFn<
95
+ TColumns extends SqliteDsl.ConstraintColumns,
96
+ TOptions extends TableOptions,
97
+ > = SqliteDsl.AnyIfConstained<
98
+ TColumns,
99
+ UseShortcut<TOptions> extends true
100
+ ? (values?: GetValForKey<SqliteDsl.FromColumns.InsertRowDecoded<TColumns>, 'value'>) => MutationEvent.Any
101
+ : (values: SqliteDsl.FromColumns.InsertRowDecoded<TColumns>) => MutationEvent.Any
102
+ >
103
+
104
+ export type UpdateMutationFn<
105
+ TColumns extends SqliteDsl.ConstraintColumns,
106
+ TOptions extends TableOptions,
107
+ > = SqliteDsl.AnyIfConstained<
108
+ TColumns,
109
+ UseShortcut<TOptions> extends true
110
+ ? (values: Partial<GetValForKey<SqliteDsl.FromColumns.RowDecoded<TColumns>, 'value'>>) => MutationEvent.Any
111
+ : (args: {
112
+ where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
113
+ values: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
114
+ }) => MutationEvent.Any
115
+ >
116
+
117
+ export type DeleteMutationFn<TColumns extends SqliteDsl.ConstraintColumns, _TOptions extends TableOptions> = (args: {
118
+ where: Partial<SqliteDsl.FromColumns.RowDecoded<TColumns>>
119
+ }) => MutationEvent.Any
120
+
121
+ type UseShortcut<TOptions extends TableOptions> = TOptions['isSingleColumn'] extends true
122
+ ? TOptions['isSingleton'] extends true
123
+ ? true
124
+ : false
125
+ : false
126
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
+ export * from './schema/system-tables.js'
1
2
  export * from './util.js'
2
- export * from './database.js'
3
- export * from './migrations.js'
3
+ export * from './adapter-types.js'
4
+ export * from './schema-management/migrations.js'
4
5
  export * from './mutation.js'
5
6
  export * from './init-singleton-tables.js'
6
7
  export * from './rehydrate-from-mutationlog.js'
8
+ export * from './query-info.js'
9
+ export * from './derived-mutations.js'
10
+ export * from './sync/index.js'
@@ -1,9 +1,9 @@
1
- import type { MainDatabase } from './database.js'
1
+ import type { InMemoryDatabase } from './adapter-types.js'
2
2
  import type { LiveStoreSchema } from './schema/index.js'
3
3
  import { DbSchema } from './schema/index.js'
4
4
  import { prepareBindValues, sql } from './util.js'
5
5
 
6
- export const initializeSingletonTables = (schema: LiveStoreSchema, db: MainDatabase) => {
6
+ export const initializeSingletonTables = (schema: LiveStoreSchema, db: InMemoryDatabase) => {
7
7
  for (const [, tableDef] of schema.tables) {
8
8
  if (tableDef.options.isSingleton) {
9
9
  const defaultValues = DbSchema.getDefaultValuesEncoded(tableDef, undefined)
@@ -0,0 +1,104 @@
1
+ import type { Schema } from '@livestore/utils/effect'
2
+
3
+ import type { DbSchema } from './schema/index.js'
4
+
5
+ /**
6
+ * Semantic information about a query with supported cases being:
7
+ * - a whole row
8
+ * - a single column value
9
+ * - a sub value in a JSON column
10
+ */
11
+ export type QueryInfo<TTableDef extends DbSchema.TableDef = DbSchema.TableDef> =
12
+ | QueryInfoNone
13
+ | QueryInfoRow<TTableDef>
14
+ | QueryInfoColJsonValue<TTableDef, GetJsonColumn<TTableDef>>
15
+ | QueryInfoCol<TTableDef, keyof TTableDef['sqliteDef']['columns']>
16
+
17
+ export type QueryInfoNone = {
18
+ _tag: 'None'
19
+ }
20
+
21
+ export type QueryInfoRow<TTableDef extends DbSchema.TableDef> = {
22
+ _tag: 'Row'
23
+ table: TTableDef
24
+ id: string
25
+ }
26
+
27
+ export type QueryInfoCol<
28
+ TTableDef extends DbSchema.TableDef,
29
+ TColName extends keyof TTableDef['sqliteDef']['columns'],
30
+ > = {
31
+ _tag: 'Col'
32
+ table: TTableDef
33
+ id: string
34
+ column: TColName
35
+ }
36
+
37
+ export type QueryInfoColJsonValue<TTableDef extends DbSchema.TableDef, TColName extends GetJsonColumn<TTableDef>> = {
38
+ _tag: 'ColJsonValue'
39
+ table: TTableDef
40
+ id: string
41
+ column: TColName
42
+ /**
43
+ * example: `$.tabs[3].items[2]` (`$` referring to the column value)
44
+ */
45
+ jsonPath: string
46
+ }
47
+
48
+ type GetJsonColumn<TTableDef extends DbSchema.TableDef> = keyof {
49
+ [ColName in keyof TTableDef['sqliteDef']['columns'] as TTableDef['sqliteDef']['columns'][ColName]['columnType'] extends 'text'
50
+ ? ColName
51
+ : never]: {}
52
+ }
53
+
54
+ export type UpdateValueForPath<TQueryInfo extends QueryInfo> = TQueryInfo extends { _tag: 'Row' }
55
+ ? Partial<DbSchema.FromTable.RowDecodedAll<TQueryInfo['table']>>
56
+ : TQueryInfo extends { _tag: 'Col' }
57
+ ? Schema.Schema.Type<TQueryInfo['table']['sqliteDef']['columns'][TQueryInfo['column']]['schema']>
58
+ : TQueryInfo extends { _tag: 'ColJsonValue' }
59
+ ? { TODO: true }
60
+ : never
61
+
62
+ // export const mutationForQueryInfo = <const TQueryInfo extends QueryInfo>(
63
+ // queryInfo: TQueryInfo,
64
+ // value: UpdateValueForPath<TQueryInfo>,
65
+ // ): RawSqlMutationEvent => {
66
+ // if (queryInfo._tag === 'ColJsonValue' || queryInfo._tag === 'None') {
67
+ // return notYetImplemented('TODO')
68
+ // }
69
+
70
+ // const sqliteTableDef = queryInfo.table.sqliteDef
71
+ // const id = queryInfo.id
72
+
73
+ // const { columnNames, bindValues } = (() => {
74
+ // if (queryInfo._tag === 'Row') {
75
+ // const columnNames = Object.keys(value)
76
+
77
+ // const partialStructSchema = queryInfo.table.schema.pipe(Schema.pick(...columnNames))
78
+
79
+ // // const columnNames = Object.keys(value)
80
+ // const encodedBindValues = Schema.encodeEither(partialStructSchema)(value)
81
+ // if (encodedBindValues._tag === 'Left') {
82
+ // return shouldNeverHappen(encodedBindValues.left.toString())
83
+ // } else {
84
+ // return { columnNames, bindValues: encodedBindValues.right }
85
+ // }
86
+ // } else if (queryInfo._tag === 'Col') {
87
+ // const columnName = queryInfo.column
88
+ // const columnSchema =
89
+ // sqliteTableDef.columns[columnName]?.schema ?? shouldNeverHappen(`Column ${columnName} not found`)
90
+ // const bindValues = { [columnName]: Schema.encodeSync(columnSchema)(value) }
91
+ // return { columnNames: [columnName], bindValues }
92
+ // } else {
93
+ // return shouldNeverHappen()
94
+ // }
95
+ // })()
96
+
97
+ // const updateClause = columnNames.map((columnName) => `${columnName} = $${columnName}`).join(', ')
98
+
99
+ // const whereClause = `where id = '${id}'`
100
+ // const sql = `UPDATE ${sqliteTableDef.name} SET ${updateClause} ${whereClause}`
101
+ // const writeTables = new Set<string>([queryInfo.table.sqliteDef.name])
102
+
103
+ // return rawSqlMutation({ sql, bindValues, writeTables })
104
+ // }
@@ -1,45 +1,52 @@
1
1
  import { shouldNeverHappen } from '@livestore/utils'
2
2
  import { Schema } from '@livestore/utils/effect'
3
3
 
4
- import type { MainDatabase } from './database.js'
4
+ import type { InMemoryDatabase, MigrationOptionsFromMutationLog } from './adapter-types.js'
5
5
  import { getExecArgsFromMutation } from './mutation.js'
6
- import type { LiveStoreSchema } from './schema/index.js'
6
+ import type { LiveStoreSchema, MutationLogMetaRow } from './schema/index.js'
7
+ import { MUTATION_LOG_META_TABLE } from './schema/index.js'
7
8
 
8
- type MutationLogRow = {
9
- id: string
10
- mutation: string
11
- args_json: string
12
- schema_hash: number
13
- created_at: string
14
- }
15
-
16
- export const rehydrateFromMutationLog = ({
9
+ export const rehydrateFromMutationLog = async ({
17
10
  logDb,
18
11
  db,
19
12
  schema,
13
+ migrationOptions,
20
14
  }: {
21
- logDb: MainDatabase
22
- db: MainDatabase
15
+ logDb: InMemoryDatabase
16
+ db: InMemoryDatabase
23
17
  schema: LiveStoreSchema
18
+ migrationOptions: MigrationOptionsFromMutationLog
24
19
  }) => {
25
20
  try {
26
- const stmt = logDb.prepare('SELECT * FROM mutation_log ORDER BY id ASC')
27
- const results = stmt.select<MutationLogRow>(undefined)
21
+ // TODO possibly implement this in a streaming fashion
22
+ const stmt = logDb.prepare(`SELECT * FROM ${MUTATION_LOG_META_TABLE} ORDER BY id ASC`)
23
+ const results = stmt.select<MutationLogMetaRow>(undefined)
28
24
 
29
25
  performance.mark('livestore:hydrate-from-mutationlog:start')
30
26
 
31
27
  for (const row of results) {
32
28
  const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
33
29
 
34
- if (Schema.hash(mutationDef.schema) !== row.schema_hash) {
35
- throw new Error(`Schema hash mismatch for mutation ${row.mutation}`)
30
+ if (migrationOptions.excludeMutations?.has(row.mutation) === true) continue
31
+
32
+ if (Schema.hash(mutationDef.schema) !== row.schemaHash) {
33
+ console.warn(`Schema hash mismatch for mutation ${row.mutation}. Trying to apply mutation anyway.`)
34
+ }
35
+
36
+ const argsDecodedEither = Schema.decodeUnknownEither(Schema.parseJson(mutationDef.schema))(row.argsJson)
37
+ if (argsDecodedEither._tag === 'Left') {
38
+ return shouldNeverHappen(`\
39
+ There was an error decoding the persisted mutation event args for mutation "${row.mutation}".
40
+ This likely means the schema has changed in an incompatible way.
41
+
42
+ Error: ${argsDecodedEither.left}
43
+ `)
36
44
  }
37
45
 
38
- const argsDecoded = Schema.decodeUnknownSync(Schema.parseJson(mutationDef.schema))(row.args_json)
39
46
  const mutationEventDecoded = {
40
47
  id: row.id,
41
48
  mutation: row.mutation,
42
- args: argsDecoded,
49
+ args: argsDecodedEither.right,
43
50
  }
44
51
  // const argsEncoded = JSON.parse(row.args_json)
45
52
  // const mutationSqlRes =
@@ -53,7 +60,14 @@ export const rehydrateFromMutationLog = ({
53
60
 
54
61
  for (const { statementSql, bindValues } of execArgsArr) {
55
62
  try {
56
- db.execute(statementSql, bindValues)
63
+ const getRowsChanged = db.execute(statementSql, bindValues)
64
+ if (
65
+ import.meta.env.DEV &&
66
+ getRowsChanged() === 0 &&
67
+ migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true
68
+ ) {
69
+ console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
70
+ }
57
71
  // console.log(`Re-executed mutation ${mutationSql}`, bindValues)
58
72
  } catch (e) {
59
73
  console.error(`Error executing migration for mutation ${statementSql}`, bindValues, e)
@@ -1,7 +1,9 @@
1
- import { isReadonlyArray } from '@livestore/utils'
1
+ import { isReadonlyArray, shouldNeverHappen } from '@livestore/utils'
2
2
  import type { ReadonlyArray } from '@livestore/utils/effect'
3
3
  import { SqliteAst, type SqliteDsl } from 'effect-db-schema'
4
4
 
5
+ import type { MigrationOptions } from '../adapter-types.js'
6
+ import { makeDerivedMutationDefsForTable } from '../derived-mutations.js'
5
7
  import {
6
8
  type MutationDef,
7
9
  type MutationDefMap,
@@ -28,6 +30,10 @@ export type LiveStoreSchema<
28
30
 
29
31
  readonly tables: Map<string, TableDef>
30
32
  readonly mutations: MutationDefMap
33
+ /** Compound hash of all table defs etc */
34
+ readonly hash: number
35
+
36
+ migrationOptions: MigrationOptions
31
37
  }
32
38
 
33
39
  export type InputSchema = {
@@ -37,20 +43,22 @@ export type InputSchema = {
37
43
 
38
44
  export const makeSchema = <TInputSchema extends InputSchema>(
39
45
  /** Note when using the object-notation for tables/mutations, the object keys are ignored and not used as table/mutation names */
40
- schema: TInputSchema,
41
- ): LiveStoreSchema<
42
- DbSchemaFromInputSchemaTables<TInputSchema['tables']>,
43
- MutationDefRecordFromInputSchemaMutations<TInputSchema['mutations']>
44
- > => {
45
- const inputTables: ReadonlyArray<TableDef> = Array.isArray(schema.tables)
46
- ? schema.tables
47
- : // TODO validate that table names are unique in this case
48
- Object.values(schema.tables)
46
+ inputSchema: TInputSchema & {
47
+ /** "hard-reset" is currently the default strategy */
48
+ migrations?: MigrationOptions<FromInputSchema.DeriveSchema<TInputSchema>>
49
+ },
50
+ ): FromInputSchema.DeriveSchema<TInputSchema> => {
51
+ const inputTables: ReadonlyArray<TableDef> = Array.isArray(inputSchema.tables)
52
+ ? inputSchema.tables
53
+ : Object.values(inputSchema.tables)
49
54
 
50
55
  const tables = new Map<string, TableDef>()
51
56
 
52
57
  for (const tableDef of inputTables) {
53
58
  // TODO validate tables (e.g. index names are unique)
59
+ if (tables.has(tableDef.sqliteDef.ast.name)) {
60
+ shouldNeverHappen(`Duplicate table name: ${tableDef.sqliteDef.ast.name}. Please use unique names for tables.`)
61
+ }
54
62
  tables.set(tableDef.sqliteDef.ast.name, tableDef)
55
63
  }
56
64
 
@@ -60,47 +68,67 @@ export const makeSchema = <TInputSchema extends InputSchema>(
60
68
 
61
69
  const mutations: MutationDefMap = new Map()
62
70
 
63
- if (isReadonlyArray(schema.mutations)) {
64
- for (const mutation of schema.mutations) {
71
+ if (isReadonlyArray(inputSchema.mutations)) {
72
+ for (const mutation of inputSchema.mutations) {
65
73
  mutations.set(mutation.name, mutation)
66
74
  }
67
75
  } else {
68
- for (const [name, mutation] of Object.entries(schema.mutations ?? {})) {
69
- mutations.set(name, mutation)
76
+ for (const mutation of Object.values(inputSchema.mutations ?? {})) {
77
+ if (mutations.has(mutation.name)) {
78
+ shouldNeverHappen(`Duplicate mutation name: ${mutation.name}. Please use unique names for mutations.`)
79
+ }
80
+ mutations.set(mutation.name, mutation)
70
81
  }
71
82
  }
72
83
 
73
- mutations.set('livestore.RawSql', rawSqlMutation)
84
+ mutations.set(rawSqlMutation.name, rawSqlMutation)
85
+
86
+ for (const tableDef of tables.values()) {
87
+ if (tableDef.options.deriveMutations) {
88
+ const derivedMutationDefs = makeDerivedMutationDefsForTable(tableDef)
89
+ mutations.set(derivedMutationDefs.insert.name, derivedMutationDefs.insert)
90
+ mutations.set(derivedMutationDefs.update.name, derivedMutationDefs.update)
91
+ mutations.set(derivedMutationDefs.delete.name, derivedMutationDefs.delete)
92
+ }
93
+ }
94
+
95
+ const hash = SqliteAst.hash({
96
+ _tag: 'dbSchema',
97
+ tables: [...tables.values()].map((_) => _.sqliteDef.ast),
98
+ })
74
99
 
75
100
  return {
76
101
  _DbSchemaType: Symbol('livestore.DbSchemaType') as any,
77
102
  _MutationDefMapType: Symbol('livestore.MutationDefMapType') as any,
78
103
  tables,
79
104
  mutations,
105
+ migrationOptions: inputSchema.migrations ?? { strategy: 'hard-reset' },
106
+ hash,
80
107
  } satisfies LiveStoreSchema
81
108
  }
82
109
 
83
- export const makeSchemaHash = (schema: LiveStoreSchema) =>
84
- SqliteAst.hash({
85
- _tag: 'dbSchema',
86
- tables: [...schema.tables.values()].map((_) => _.sqliteDef.ast),
87
- })
88
-
89
- /**
90
- * In case of ...
91
- * - array: we use the table name of each array item (= table definition) as the object key
92
- * - object: we discard the keys of the input object and use the table name of each object value (= table definition) as the new object key
93
- */
94
- export type DbSchemaFromInputSchemaTables<TTables extends InputSchema['tables']> =
95
- TTables extends ReadonlyArray<TableDef>
96
- ? { [K in TTables[number] as K['sqliteDef']['name']]: K['sqliteDef'] }
97
- : TTables extends Record<string, TableDef>
98
- ? { [K in keyof TTables as TTables[K]['sqliteDef']['name']]: TTables[K]['sqliteDef'] }
99
- : never
100
-
101
- export type MutationDefRecordFromInputSchemaMutations<TMutations extends InputSchema['mutations']> =
102
- TMutations extends ReadonlyArray<MutationDef.Any>
103
- ? { [K in TMutations[number] as K['name']]: K } & { 'livestore.RawSql': RawSqlMutation }
104
- : TMutations extends { [name: string]: MutationDef.Any }
105
- ? { [K in keyof TMutations as TMutations[K]['name']]: TMutations[K] } & { 'livestore.RawSql': RawSqlMutation }
106
- : never
110
+ namespace FromInputSchema {
111
+ export type DeriveSchema<TInputSchema extends InputSchema> = LiveStoreSchema<
112
+ DbSchemaFromInputSchemaTables<TInputSchema['tables']>,
113
+ MutationDefRecordFromInputSchemaMutations<TInputSchema['mutations']>
114
+ >
115
+
116
+ /**
117
+ * In case of ...
118
+ * - array: we use the table name of each array item (= table definition) as the object key
119
+ * - object: we discard the keys of the input object and use the table name of each object value (= table definition) as the new object key
120
+ */
121
+ type DbSchemaFromInputSchemaTables<TTables extends InputSchema['tables']> =
122
+ TTables extends ReadonlyArray<TableDef>
123
+ ? { [K in TTables[number] as K['sqliteDef']['name']]: K['sqliteDef'] }
124
+ : TTables extends Record<string, TableDef>
125
+ ? { [K in keyof TTables as TTables[K]['sqliteDef']['name']]: TTables[K]['sqliteDef'] }
126
+ : never
127
+
128
+ type MutationDefRecordFromInputSchemaMutations<TMutations extends InputSchema['mutations']> =
129
+ TMutations extends ReadonlyArray<MutationDef.Any>
130
+ ? { [K in TMutations[number] as K['name']]: K } & { 'livestore.RawSql': RawSqlMutation }
131
+ : TMutations extends { [name: string]: MutationDef.Any }
132
+ ? { [K in keyof TMutations as TMutations[K]['name']]: TMutations[K] } & { 'livestore.RawSql': RawSqlMutation }
133
+ : never
134
+ }
@@ -77,10 +77,10 @@ export const makeMutationDefRecord = <TInputRecord extends Record<string, Mutati
77
77
 
78
78
  export const rawSqlMutation = defineMutation(
79
79
  'livestore.RawSql',
80
- Schema.struct({
81
- sql: Schema.string,
82
- bindValues: Schema.optional(Schema.record(Schema.string, Schema.any)),
83
- writeTables: Schema.optional(Schema.readonlySet(Schema.string)),
80
+ Schema.Struct({
81
+ sql: Schema.String,
82
+ bindValues: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
83
+ writeTables: Schema.optional(Schema.ReadonlySet(Schema.String)),
84
84
  }),
85
85
  ({ sql, bindValues, writeTables }) => ({ sql, bindValues: bindValues ?? {}, writeTables }),
86
86
  )
@@ -94,8 +94,15 @@ export type MutationEvent<TMutationsDef extends MutationDef.Any> = {
94
94
  id: string
95
95
  }
96
96
 
97
+ export type MutationEventEncoded<TMutationsDef extends MutationDef.Any> = {
98
+ mutation: TMutationsDef['name']
99
+ args: Schema.Schema.Encoded<TMutationsDef['schema']>
100
+ id: string
101
+ }
102
+
97
103
  export namespace MutationEvent {
98
104
  export type Any = MutationEvent<MutationDef.Any>
105
+ export type AnyEncoded = MutationEventEncoded<MutationDef.Any>
99
106
 
100
107
  export type ForSchema<TSchema extends LiveStoreSchema> = {
101
108
  [K in keyof TSchema['_MutationDefMapType']]: MutationEvent<TSchema['_MutationDefMapType'][K]>
@@ -122,12 +129,24 @@ export type MutationEventSchema<TMutationsDefRecord extends MutationDefRecord> =
122
129
  export const makeMutationEventSchema = <TMutationsDefRecord extends MutationDefRecord>(
123
130
  mutationDefRecord: TMutationsDefRecord,
124
131
  ): MutationEventSchema<TMutationsDefRecord> =>
125
- Schema.union(
132
+ Schema.Union(
126
133
  ...Object.values(mutationDefRecord).map((def) =>
127
- Schema.struct({
128
- mutation: Schema.literal(def.name),
134
+ Schema.Struct({
135
+ mutation: Schema.Literal(def.name),
129
136
  args: def.schema,
130
- id: Schema.string,
137
+ id: Schema.String,
131
138
  }),
132
139
  ),
133
- ) as any
140
+ ).annotations({ title: 'MutationEventSchema' }) as any
141
+
142
+ export const mutationEventSchemaDecodedAny = Schema.Struct({
143
+ mutation: Schema.String,
144
+ args: Schema.Any,
145
+ id: Schema.String,
146
+ }).annotations({ title: 'MutationEventSchema.DecodedAny' })
147
+
148
+ export const mutationEventSchemaEncodedAny = Schema.Struct({
149
+ mutation: Schema.String,
150
+ args: Schema.Any,
151
+ id: Schema.String,
152
+ }).annotations({ title: 'MutationEventSchema.EncodedAny' })