@livestore/common 0.0.54-dev.22 → 0.0.54-dev.23
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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/fixture.d.ts +0 -2
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/adapter-types.d.ts +59 -4
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +16 -11
- package/dist/adapter-types.js.map +1 -1
- package/dist/devtools/devtools-messages.d.ts +41 -39
- package/dist/devtools/devtools-messages.d.ts.map +1 -1
- package/dist/devtools/devtools-messages.js +27 -28
- package/dist/devtools/devtools-messages.js.map +1 -1
- package/dist/devtools/devtools-window-message.d.ts +26 -0
- package/dist/devtools/devtools-window-message.d.ts.map +1 -0
- package/dist/devtools/devtools-window-message.js +30 -0
- package/dist/devtools/devtools-window-message.js.map +1 -0
- package/dist/devtools/index.d.ts +1 -0
- package/dist/devtools/index.d.ts.map +1 -1
- package/dist/devtools/index.js +1 -0
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/rehydrate-from-mutationlog.d.ts +8 -3
- package/dist/rehydrate-from-mutationlog.d.ts.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +71 -56
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/system-tables.d.ts +3 -8
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/table-def.d.ts +0 -2
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +0 -1
- package/dist/schema/table-def.js.map +1 -1
- package/dist/schema-management/migrations.d.ts +9 -4
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +24 -13
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -3
- package/src/adapter-types.ts +46 -15
- package/src/devtools/devtools-messages.ts +55 -48
- package/src/devtools/devtools-window-message.ts +25 -0
- package/src/devtools/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/rehydrate-from-mutationlog.ts +100 -64
- package/src/schema/table-def.ts +0 -4
- package/src/schema-management/migrations.ts +104 -84
- package/src/version.ts +3 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Schema } from '@livestore/utils/effect'
|
|
3
|
-
import { type SqliteDsl as __SqliteDsl } from 'effect-db-schema'
|
|
1
|
+
import { Schema, Transferable } from '@livestore/utils/effect'
|
|
4
2
|
|
|
5
3
|
import { NetworkStatus } from '../adapter-types.js'
|
|
6
4
|
import { DebugInfo } from '../debug-info.js'
|
|
7
5
|
import { mutationEventSchemaEncodedAny } from '../schema/mutations.js'
|
|
8
6
|
import { PreparedBindValues } from '../util.js'
|
|
7
|
+
import { liveStoreVersion as pkgVersion } from '../version.js'
|
|
9
8
|
|
|
10
9
|
const requestId = Schema.String
|
|
11
10
|
const channelId = Schema.String
|
|
@@ -20,20 +19,21 @@ export class SnapshotReq extends Schema.TaggedStruct('LSD.SnapshotReq', {
|
|
|
20
19
|
export class SnapshotRes extends Schema.TaggedStruct('LSD.SnapshotRes', {
|
|
21
20
|
liveStoreVersion,
|
|
22
21
|
requestId,
|
|
23
|
-
snapshot:
|
|
22
|
+
snapshot: Transferable.Uint8Array,
|
|
24
23
|
}).annotations({ identifier: 'LSD.SnapshotRes' }) {}
|
|
25
24
|
|
|
26
|
-
export class
|
|
25
|
+
export class LoadDatabaseFileReq extends Schema.TaggedStruct('LSD.LoadDatabaseFileReq', {
|
|
27
26
|
liveStoreVersion,
|
|
28
27
|
requestId,
|
|
29
28
|
channelId,
|
|
30
|
-
|
|
31
|
-
}).annotations({ identifier: 'LSD.
|
|
29
|
+
data: Transferable.Uint8Array,
|
|
30
|
+
}).annotations({ identifier: 'LSD.LoadDatabaseFileReq' }) {}
|
|
32
31
|
|
|
33
|
-
export class
|
|
32
|
+
export class LoadDatabaseFileRes extends Schema.TaggedStruct('LSD.LoadDatabaseFileRes', {
|
|
34
33
|
liveStoreVersion,
|
|
35
34
|
requestId,
|
|
36
|
-
|
|
35
|
+
status: Schema.Literal('ok', 'unsupported-file', 'unsupported-database'),
|
|
36
|
+
}).annotations({ identifier: 'LSD.LoadDatabaseFileRes' }) {}
|
|
37
37
|
|
|
38
38
|
export class DebugInfoReq extends Schema.TaggedStruct('LSD.DebugInfoReq', {
|
|
39
39
|
liveStoreVersion,
|
|
@@ -102,21 +102,9 @@ export class MutationLogRes extends Schema.TaggedStruct('LSD.MutationLogRes', {
|
|
|
102
102
|
liveStoreVersion,
|
|
103
103
|
requestId,
|
|
104
104
|
channelId,
|
|
105
|
-
mutationLog:
|
|
105
|
+
mutationLog: Transferable.Uint8Array,
|
|
106
106
|
}).annotations({ identifier: 'LSD.MutationLogRes' }) {}
|
|
107
107
|
|
|
108
|
-
export class LoadMutationLogReq extends Schema.TaggedStruct('LSD.LoadMutationLogReq', {
|
|
109
|
-
liveStoreVersion,
|
|
110
|
-
requestId,
|
|
111
|
-
channelId,
|
|
112
|
-
mutationLog: Schema.Uint8Array,
|
|
113
|
-
}).annotations({ identifier: 'LSD.LoadMutationLogReq' }) {}
|
|
114
|
-
|
|
115
|
-
export class LoadMutationLogRes extends Schema.TaggedStruct('LSD.LoadMutationLogRes', {
|
|
116
|
-
liveStoreVersion,
|
|
117
|
-
requestId,
|
|
118
|
-
}).annotations({ identifier: 'LSD.LoadMutationLogRes' }) {}
|
|
119
|
-
|
|
120
108
|
export class ReactivityGraphSubscribe extends Schema.TaggedStruct('LSD.ReactivityGraphSubscribe', {
|
|
121
109
|
liveStoreVersion,
|
|
122
110
|
requestId,
|
|
@@ -178,6 +166,19 @@ export class ResetAllDataRes extends Schema.TaggedStruct('LSD.ResetAllDataRes',
|
|
|
178
166
|
requestId,
|
|
179
167
|
}).annotations({ identifier: 'LSD.ResetAllDataRes' }) {}
|
|
180
168
|
|
|
169
|
+
export class MessagePortForStoreReq extends Schema.TaggedStruct('LSD.MessagePortForStoreReq', {
|
|
170
|
+
liveStoreVersion,
|
|
171
|
+
requestId,
|
|
172
|
+
channelId,
|
|
173
|
+
}).annotations({ identifier: 'LSD.MessagePortForStoreReq' }) {}
|
|
174
|
+
|
|
175
|
+
export class MessagePortForStoreRes extends Schema.TaggedStruct('LSD.MessagePortForStoreRes', {
|
|
176
|
+
liveStoreVersion,
|
|
177
|
+
requestId,
|
|
178
|
+
channelId,
|
|
179
|
+
port: Transferable.MessagePort,
|
|
180
|
+
}).annotations({ identifier: 'LSD.MessagePortForStoreRes' }) {}
|
|
181
|
+
|
|
181
182
|
export class NetworkStatusChanged extends Schema.TaggedStruct('LSD.NetworkStatusChanged', {
|
|
182
183
|
liveStoreVersion,
|
|
183
184
|
channelId,
|
|
@@ -215,11 +216,22 @@ export class Pong extends Schema.TaggedStruct('LSD.Pong', {
|
|
|
215
216
|
requestId,
|
|
216
217
|
}).annotations({ identifier: 'LSD.Pong' }) {}
|
|
217
218
|
|
|
218
|
-
export const
|
|
219
|
+
export const MessageToAppHostCoordinator = Schema.Union(
|
|
219
220
|
SnapshotReq,
|
|
220
|
-
|
|
221
|
+
LoadDatabaseFileReq,
|
|
221
222
|
MutationLogReq,
|
|
222
|
-
|
|
223
|
+
ResetAllDataReq,
|
|
224
|
+
MessagePortForStoreRes,
|
|
225
|
+
DevtoolsReady,
|
|
226
|
+
Disconnect,
|
|
227
|
+
DevtoolsConnected,
|
|
228
|
+
RunMutationReq,
|
|
229
|
+
Ping,
|
|
230
|
+
).annotations({ identifier: 'LSD.MessageToAppHostCoordinator' })
|
|
231
|
+
|
|
232
|
+
export type MessageToAppHostCoordinator = typeof MessageToAppHostCoordinator.Type
|
|
233
|
+
|
|
234
|
+
export const MessageToAppHostStore = Schema.Union(
|
|
223
235
|
DebugInfoReq,
|
|
224
236
|
DebugInfoResetReq,
|
|
225
237
|
DebugInfoRerunQueryReq,
|
|
@@ -227,39 +239,34 @@ export const MessageToAppHost = Schema.Union(
|
|
|
227
239
|
ReactivityGraphUnsubscribe,
|
|
228
240
|
LiveQueriesSubscribe,
|
|
229
241
|
LiveQueriesUnsubscribe,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Disconnect,
|
|
233
|
-
DevtoolsConnected,
|
|
234
|
-
RunMutationReq,
|
|
235
|
-
Ping,
|
|
236
|
-
).annotations({ identifier: 'LSD.MessageToAppHost' })
|
|
242
|
+
// Ping,
|
|
243
|
+
).annotations({ identifier: 'LSD.MessageToAppHostStore' })
|
|
237
244
|
|
|
238
|
-
export type
|
|
245
|
+
export type MessageToAppHostStore = typeof MessageToAppHostStore.Type
|
|
239
246
|
|
|
240
|
-
export const
|
|
247
|
+
export const MessageFromAppHostCoordinator = Schema.Union(
|
|
241
248
|
SnapshotRes,
|
|
242
|
-
|
|
249
|
+
LoadDatabaseFileRes,
|
|
243
250
|
MutationLogRes,
|
|
244
|
-
LoadMutationLogRes,
|
|
245
|
-
DebugInfoRes,
|
|
246
|
-
DebugInfoResetRes,
|
|
247
|
-
DebugInfoRerunQueryRes,
|
|
248
|
-
ReactivityGraphRes,
|
|
249
|
-
LiveQueriesRes,
|
|
250
251
|
ResetAllDataRes,
|
|
252
|
+
MessagePortForStoreReq,
|
|
251
253
|
Disconnect,
|
|
252
254
|
MutationBroadcast,
|
|
253
255
|
AppHostReady,
|
|
254
256
|
NetworkStatusChanged,
|
|
255
257
|
RunMutationRes,
|
|
256
258
|
Pong,
|
|
257
|
-
).annotations({ identifier: 'LSD.
|
|
259
|
+
).annotations({ identifier: 'LSD.MessageFromAppHostCoordinator' })
|
|
260
|
+
|
|
261
|
+
export type MessageFromAppHostCoordinator = typeof MessageFromAppHostCoordinator.Type
|
|
258
262
|
|
|
259
|
-
export
|
|
263
|
+
export const MessageFromAppHostStore = Schema.Union(
|
|
264
|
+
DebugInfoRes,
|
|
265
|
+
DebugInfoResetRes,
|
|
266
|
+
DebugInfoRerunQueryRes,
|
|
267
|
+
ReactivityGraphRes,
|
|
268
|
+
LiveQueriesRes,
|
|
269
|
+
// Pong,
|
|
270
|
+
).annotations({ identifier: 'LSD.MessageFromAppHostStore' })
|
|
260
271
|
|
|
261
|
-
|
|
262
|
-
export const makeBroadcastChannels = () => ({
|
|
263
|
-
fromAppHost: new BroadcastChannel(`livestore-devtools-from-app-host`),
|
|
264
|
-
toAppHost: new BroadcastChannel(`livestore-devtools-to-app-host`),
|
|
265
|
-
})
|
|
272
|
+
export type MessageFromAppHostStore = typeof MessageFromAppHostStore.Type
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Schema, Transferable } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
const channelId = Schema.String
|
|
4
|
+
|
|
5
|
+
export namespace DevtoolsWindowMessage {
|
|
6
|
+
/** Message is being created in contentscript-iframe, sent to contentscript and then sent to Store */
|
|
7
|
+
export class MessagePortReady extends Schema.TaggedStruct('LSD.WindowMessage.MessagePortForStore', {
|
|
8
|
+
port: Transferable.MessagePort,
|
|
9
|
+
channelId,
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export class ContentscriptListening extends Schema.TaggedStruct('LSD.WindowMessage.ContentscriptListening', {}) {}
|
|
13
|
+
|
|
14
|
+
// export class ContentscriptReady extends Schema.TaggedStruct('LSD.WindowMessage.ContentscriptReady', {
|
|
15
|
+
// channelId,
|
|
16
|
+
// }) {}
|
|
17
|
+
|
|
18
|
+
export class StoreReady extends Schema.TaggedStruct('LSD.WindowMessage.StoreReady', {
|
|
19
|
+
channelId,
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
export class MessageForStore extends Schema.Union(MessagePortReady, ContentscriptListening) {}
|
|
23
|
+
|
|
24
|
+
export class MessageForContentscript extends Schema.Union(StoreReady) {}
|
|
25
|
+
}
|
package/src/devtools/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,91 +1,127 @@
|
|
|
1
1
|
import { shouldNeverHappen } from '@livestore/utils'
|
|
2
|
-
import { Schema } from '@livestore/utils/effect'
|
|
2
|
+
import { Chunk, Effect, Option, Schema, Stream } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
|
-
import type
|
|
4
|
+
import { type InMemoryDatabase, type MigrationOptionsFromMutationLog, SqliteError } from './adapter-types.js'
|
|
5
5
|
import { getExecArgsFromMutation } from './mutation.js'
|
|
6
6
|
import type { LiveStoreSchema, MutationLogMetaRow } from './schema/index.js'
|
|
7
7
|
import { MUTATION_LOG_META_TABLE } from './schema/index.js'
|
|
8
|
+
import type { PreparedBindValues } from './util.js'
|
|
9
|
+
import { sql } from './util.js'
|
|
8
10
|
|
|
9
|
-
export const rehydrateFromMutationLog =
|
|
11
|
+
export const rehydrateFromMutationLog = ({
|
|
10
12
|
logDb,
|
|
11
13
|
db,
|
|
12
14
|
schema,
|
|
13
15
|
migrationOptions,
|
|
16
|
+
onProgress,
|
|
14
17
|
}: {
|
|
15
18
|
logDb: InMemoryDatabase
|
|
16
19
|
db: InMemoryDatabase
|
|
17
20
|
schema: LiveStoreSchema
|
|
18
21
|
migrationOptions: MigrationOptionsFromMutationLog
|
|
19
|
-
}) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
onProgress: (_: { done: number; total: number }) => Effect.Effect<void>
|
|
23
|
+
}) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const mutationsCount = logDb
|
|
26
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${MUTATION_LOG_META_TABLE}`)
|
|
27
|
+
.select<{ count: number }>(undefined)[0]!.count
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
const processMutation = (row: MutationLogMetaRow) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
const mutationDef = schema.mutations.get(row.mutation) ?? shouldNeverHappen(`Unknown mutation ${row.mutation}`)
|
|
33
|
+
if (migrationOptions.excludeMutations?.has(row.mutation) === true) return
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.warn(`Schema hash mismatch for mutation ${row.mutation}. Trying to apply mutation anyway.`)
|
|
34
|
-
}
|
|
35
|
+
if (Schema.hash(mutationDef.schema) !== row.schemaHash) {
|
|
36
|
+
console.warn(`Schema hash mismatch for mutation ${row.mutation}. Trying to apply mutation anyway.`)
|
|
37
|
+
}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const argsDecodedEither = Schema.decodeUnknownEither(Schema.parseJson(mutationDef.schema))(row.argsJson)
|
|
40
|
+
if (argsDecodedEither._tag === 'Left') {
|
|
41
|
+
return shouldNeverHappen(`\
|
|
39
42
|
There was an error decoding the persisted mutation event args for mutation "${row.mutation}".
|
|
40
43
|
This likely means the schema has changed in an incompatible way.
|
|
41
44
|
|
|
42
45
|
Error: ${argsDecodedEither.left}
|
|
43
46
|
`)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mutationEventDecoded = {
|
|
50
|
+
id: row.id,
|
|
51
|
+
mutation: row.mutation,
|
|
52
|
+
args: argsDecodedEither.right,
|
|
53
|
+
}
|
|
54
|
+
// const argsEncoded = JSON.parse(row.args_json)
|
|
55
|
+
// const mutationSqlRes =
|
|
56
|
+
// typeof mutation.sql === 'string'
|
|
57
|
+
// ? mutation.sql
|
|
58
|
+
// : mutation.sql(Schema.decodeUnknownSync(mutation.schema)(argsEncoded))
|
|
59
|
+
// const mutationSql = typeof mutationSqlRes === 'string' ? mutationSqlRes : mutationSqlRes.sql
|
|
60
|
+
// const bindValues = typeof mutationSqlRes === 'string' ? argsEncoded : mutationSqlRes.bindValues
|
|
61
|
+
|
|
62
|
+
const execArgsArr = getExecArgsFromMutation({ mutationDef, mutationEventDecoded })
|
|
63
|
+
|
|
64
|
+
for (const { statementSql, bindValues } of execArgsArr) {
|
|
65
|
+
try {
|
|
66
|
+
// TODO cache prepared statements for mutations
|
|
67
|
+
const getRowsChanged = db.execute(statementSql, bindValues)
|
|
68
|
+
if (
|
|
69
|
+
import.meta.env.DEV &&
|
|
70
|
+
getRowsChanged() === 0 &&
|
|
71
|
+
migrationOptions.logging?.excludeAffectedRows?.(statementSql) !== true
|
|
72
|
+
) {
|
|
73
|
+
console.warn(`Mutation "${mutationDef.name}" did not affect any rows:`, statementSql, bindValues)
|
|
74
|
+
}
|
|
75
|
+
// console.log(`Re-executed mutation ${mutationSql}`, bindValues)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
yield* new SqliteError({
|
|
78
|
+
sql: statementSql,
|
|
79
|
+
bindValues,
|
|
80
|
+
code: (e as any).resultCode,
|
|
81
|
+
cause: e,
|
|
82
|
+
})
|
|
70
83
|
}
|
|
71
|
-
// console.log(`Re-executed mutation ${mutationSql}`, bindValues)
|
|
72
|
-
} catch (e) {
|
|
73
|
-
console.error(`Error executing migration for mutation ${statementSql}`, bindValues, e)
|
|
74
|
-
debugger
|
|
75
|
-
throw e
|
|
76
84
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
}).pipe(Effect.withSpan(`@livestore/common:rehydrateFromMutationLog:processMutation`))
|
|
86
|
+
|
|
87
|
+
const CHUNK_SIZE = 100
|
|
88
|
+
|
|
89
|
+
const stmt = logDb.prepare(sql`\
|
|
90
|
+
SELECT * FROM ${MUTATION_LOG_META_TABLE}
|
|
91
|
+
WHERE id > COALESCE($id, '')
|
|
92
|
+
ORDER BY id ASC
|
|
93
|
+
LIMIT ${CHUNK_SIZE}
|
|
94
|
+
`)
|
|
95
|
+
|
|
96
|
+
let processedMutations = 0
|
|
97
|
+
|
|
98
|
+
yield* Stream.unfoldChunk<Chunk.Chunk<MutationLogMetaRow> | { _tag: 'Initial ' }, MutationLogMetaRow>(
|
|
99
|
+
{ _tag: 'Initial ' },
|
|
100
|
+
(item) => {
|
|
101
|
+
// End stream if no more rows
|
|
102
|
+
if (Chunk.isChunk(item) && item.length === 0) return Option.none()
|
|
103
|
+
|
|
104
|
+
const lastId = Chunk.isChunk(item) ? Chunk.last(item).pipe(Option.getOrUndefined)?.id : undefined
|
|
105
|
+
const nextItem = Chunk.fromIterable(
|
|
106
|
+
stmt.select<MutationLogMetaRow>({ $id: lastId } as any as PreparedBindValues),
|
|
107
|
+
)
|
|
108
|
+
const prevItem = Chunk.isChunk(item) ? item : Chunk.empty()
|
|
109
|
+
return Option.some([prevItem, nextItem])
|
|
110
|
+
},
|
|
111
|
+
).pipe(
|
|
112
|
+
(_) => _,
|
|
113
|
+
Stream.bufferChunks({ capacity: 2 }),
|
|
114
|
+
Stream.tap((row) =>
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
yield* processMutation(row)
|
|
117
|
+
|
|
118
|
+
processedMutations++
|
|
119
|
+
yield* onProgress({ done: processedMutations, total: mutationsCount })
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
Stream.runDrain,
|
|
89
123
|
)
|
|
90
|
-
}
|
|
91
|
-
|
|
124
|
+
}).pipe(
|
|
125
|
+
Effect.withPerformanceMeasure('@livestore/common:rehydrateFromMutationLog'),
|
|
126
|
+
Effect.withSpan('@livestore/common:rehydrateFromMutationLog'),
|
|
127
|
+
)
|
package/src/schema/table-def.ts
CHANGED
|
@@ -86,8 +86,6 @@ export type TableOptions = {
|
|
|
86
86
|
* @default false
|
|
87
87
|
*/
|
|
88
88
|
isSingleton: boolean
|
|
89
|
-
// TODO remove
|
|
90
|
-
dynamicRegistration: boolean
|
|
91
89
|
disableAutomaticIdColumn: boolean
|
|
92
90
|
/**
|
|
93
91
|
* Setting this to true will automatically derive insert, update and delete mutations for this table. Example:
|
|
@@ -139,7 +137,6 @@ export const table = <
|
|
|
139
137
|
|
|
140
138
|
const options_: TableOptions = {
|
|
141
139
|
isSingleton: options?.isSingleton ?? false,
|
|
142
|
-
dynamicRegistration: options?.dynamicRegistration ?? false,
|
|
143
140
|
disableAutomaticIdColumn: options?.disableAutomaticIdColumn ?? false,
|
|
144
141
|
deriveMutations:
|
|
145
142
|
options?.deriveMutations === true
|
|
@@ -239,7 +236,6 @@ type WithId<TColumns extends SqliteDsl.Columns, TOptions extends TableOptions> =
|
|
|
239
236
|
|
|
240
237
|
type WithDefaults<TOptionsInput extends TableOptionsInput, TIsSingleColumn extends boolean> = {
|
|
241
238
|
isSingleton: TOptionsInput['isSingleton'] extends true ? true : false
|
|
242
|
-
dynamicRegistration: TOptionsInput['dynamicRegistration'] extends true ? true : false
|
|
243
239
|
disableAutomaticIdColumn: TOptionsInput['disableAutomaticIdColumn'] extends true ? true : false
|
|
244
240
|
deriveMutations: TOptionsInput['deriveMutations'] extends true
|
|
245
241
|
? { enabled: true; localOnly: boolean }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { memoizeByStringifyArgs } from '@livestore/utils'
|
|
2
|
-
import { Schema as EffectSchema } from '@livestore/utils/effect'
|
|
2
|
+
import { Effect, Schema as EffectSchema } from '@livestore/utils/effect'
|
|
3
3
|
import * as otel from '@opentelemetry/api'
|
|
4
4
|
import { SqliteAst, SqliteDsl } from 'effect-db-schema'
|
|
5
5
|
|
|
@@ -20,85 +20,104 @@ import { validateSchema } from './validate-mutation-defs.js'
|
|
|
20
20
|
|
|
21
21
|
const getMemoizedTimestamp = memoizeByStringifyArgs(() => new Date().toISOString())
|
|
22
22
|
|
|
23
|
-
export const makeSchemaManager = (db: InMemoryDatabase): SchemaManager =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
export const makeSchemaManager = (db: InMemoryDatabase): Effect.Effect<SchemaManager> =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
yield* migrateTable({
|
|
26
|
+
db,
|
|
27
|
+
otelContext: otel.context.active(),
|
|
28
|
+
tableAst: schemaMutationsMetaTable.sqliteDef.ast,
|
|
29
|
+
behaviour: 'create-if-not-exists',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
getMutationDefInfos: () => {
|
|
34
|
+
const schemaMutationsMetaRows = dbSelect<SchemaMutationsMetaRow>(
|
|
35
|
+
db,
|
|
36
|
+
sql`SELECT * FROM ${SCHEMA_MUTATIONS_META_TABLE}`,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return schemaMutationsMetaRows
|
|
40
|
+
},
|
|
41
|
+
setMutationDefInfo: (info) => {
|
|
42
|
+
dbExecute(
|
|
43
|
+
db,
|
|
44
|
+
sql`INSERT OR REPLACE INTO ${SCHEMA_MUTATIONS_META_TABLE} (mutationName, schemaHash, updatedAt) VALUES ($mutationName, $schemaHash, $updatedAt)`,
|
|
45
|
+
{
|
|
46
|
+
mutationName: info.mutationName,
|
|
47
|
+
schemaHash: info.schemaHash,
|
|
48
|
+
updatedAt: new Date().toISOString(),
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
},
|
|
52
|
+
}
|
|
29
53
|
})
|
|
30
54
|
|
|
31
|
-
return {
|
|
32
|
-
getMutationDefInfos: () => {
|
|
33
|
-
const schemaMutationsMetaRows = dbSelect<SchemaMutationsMetaRow>(
|
|
34
|
-
db,
|
|
35
|
-
sql`SELECT * FROM ${SCHEMA_MUTATIONS_META_TABLE}`,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
return schemaMutationsMetaRows
|
|
39
|
-
},
|
|
40
|
-
setMutationDefInfo: (info) => {
|
|
41
|
-
dbExecute(
|
|
42
|
-
db,
|
|
43
|
-
sql`INSERT OR REPLACE INTO ${SCHEMA_MUTATIONS_META_TABLE} (mutationName, schemaHash, updatedAt) VALUES ($mutationName, $schemaHash, $updatedAt)`,
|
|
44
|
-
{
|
|
45
|
-
mutationName: info.mutationName,
|
|
46
|
-
schemaHash: info.schemaHash,
|
|
47
|
-
updatedAt: new Date().toISOString(),
|
|
48
|
-
},
|
|
49
|
-
)
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
55
|
// TODO more graceful DB migration (e.g. backup DB before destructive migrations)
|
|
55
56
|
export const migrateDb = ({
|
|
56
57
|
db,
|
|
57
58
|
otelContext = otel.context.active(),
|
|
58
59
|
schema,
|
|
60
|
+
onProgress,
|
|
59
61
|
}: {
|
|
60
62
|
db: InMemoryDatabase
|
|
61
63
|
otelContext?: otel.Context
|
|
62
64
|
schema: LiveStoreSchema
|
|
63
|
-
}) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
onProgress?: (opts: { done: number; total: number }) => Effect.Effect<void>
|
|
66
|
+
}) =>
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
yield* migrateTable({
|
|
69
|
+
db,
|
|
70
|
+
otelContext,
|
|
71
|
+
tableAst: schemaMetaTable.sqliteDef.ast,
|
|
72
|
+
behaviour: 'create-if-not-exists',
|
|
73
|
+
})
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
// TODO enforce that migrating tables isn't allowed once the store is running
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
const schemaManager = yield* makeSchemaManager(db)
|
|
78
|
+
validateSchema(schema, schemaManager)
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
|
|
77
|
-
)
|
|
80
|
+
const schemaMetaRows = dbSelect<SchemaMetaRow>(db, sql`SELECT * FROM ${SCHEMA_META_TABLE}`)
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
...Array.from(schema.tables.values()).filter((_) => _.sqliteDef.name !== SCHEMA_META_TABLE),
|
|
83
|
-
])
|
|
82
|
+
const dbSchemaHashByTable = Object.fromEntries(
|
|
83
|
+
schemaMetaRows.map(({ tableName, schemaHash }) => [tableName, schemaHash]),
|
|
84
|
+
)
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const tableDefs = new Set([
|
|
87
|
+
// NOTE it's important the `SCHEMA_META_TABLE` comes first since we're writing to it below
|
|
88
|
+
...systemTables,
|
|
89
|
+
...Array.from(schema.tables.values()).filter((_) => _.sqliteDef.name !== SCHEMA_META_TABLE),
|
|
90
|
+
])
|
|
90
91
|
|
|
91
|
-
const
|
|
92
|
+
const tablesToMigrate = new Set<{ tableAst: SqliteAst.Table; schemaHash: number }>()
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
for (const tableDef of tableDefs) {
|
|
95
|
+
const tableAst = tableDef.sqliteDef.ast
|
|
96
|
+
const tableName = tableAst.name
|
|
97
|
+
const dbSchemaHash = dbSchemaHashByTable[tableName]
|
|
98
|
+
const schemaHash = SqliteAst.hash(tableAst)
|
|
99
|
+
|
|
100
|
+
if (schemaHash !== dbSchemaHash) {
|
|
101
|
+
tablesToMigrate.add({ tableAst, schemaHash })
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
console.log(
|
|
104
|
+
`Schema hash mismatch for table '${tableName}' (DB: ${dbSchemaHash}, expected: ${schemaHash}), migrating table...`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
|
|
109
|
+
let processedTables = 0
|
|
110
|
+
const tablesCount = tablesToMigrate.size
|
|
111
|
+
|
|
112
|
+
for (const { tableAst, schemaHash } of tablesToMigrate) {
|
|
113
|
+
yield* migrateTable({ db, tableAst, otelContext, schemaHash, behaviour: 'create-if-not-exists' })
|
|
114
|
+
|
|
115
|
+
if (onProgress !== undefined) {
|
|
116
|
+
processedTables++
|
|
117
|
+
yield* onProgress({ done: processedTables, total: tablesCount })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
102
121
|
|
|
103
122
|
export const migrateTable = ({
|
|
104
123
|
db,
|
|
@@ -114,36 +133,37 @@ export const migrateTable = ({
|
|
|
114
133
|
schemaHash?: number
|
|
115
134
|
behaviour: 'drop-and-recreate' | 'create-if-not-exists'
|
|
116
135
|
skipMetaTable?: boolean
|
|
117
|
-
}) =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
}) =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
console.log(`Migrating table '${tableAst.name}'...`)
|
|
139
|
+
const tableName = tableAst.name
|
|
140
|
+
const columnSpec = makeColumnSpec(tableAst)
|
|
141
|
+
|
|
142
|
+
if (behaviour === 'drop-and-recreate') {
|
|
143
|
+
// TODO need to possibly handle cascading deletes due to foreign keys
|
|
144
|
+
dbExecute(db, sql`drop table if exists ${tableName}`)
|
|
145
|
+
dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
|
|
146
|
+
} else if (behaviour === 'create-if-not-exists') {
|
|
147
|
+
dbExecute(db, sql`create table if not exists ${tableName} (${columnSpec}) strict`)
|
|
148
|
+
}
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
150
|
+
for (const index of tableAst.indexes) {
|
|
151
|
+
dbExecute(db, createIndexFromDefinition(tableName, index))
|
|
152
|
+
}
|
|
133
153
|
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
if (skipMetaTable !== true) {
|
|
155
|
+
const updatedAt = getMemoizedTimestamp()
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
dbExecute(
|
|
158
|
+
db,
|
|
159
|
+
sql`
|
|
140
160
|
INSERT INTO ${SCHEMA_META_TABLE} (tableName, schemaHash, updatedAt) VALUES ($tableName, $schemaHash, $updatedAt)
|
|
141
161
|
ON CONFLICT (tableName) DO UPDATE SET schemaHash = $schemaHash, updatedAt = $updatedAt;
|
|
142
162
|
`,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
163
|
+
{ tableName, schemaHash, updatedAt },
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}).pipe(Effect.withSpan('@livestore/common:migrateTable', { attributes: { tableName: tableAst.name } }))
|
|
147
167
|
|
|
148
168
|
const createIndexFromDefinition = (tableName: string, index: SqliteAst.Index) => {
|
|
149
169
|
const uniqueStr = index.unique ? 'UNIQUE' : ''
|
package/src/version.ts
ADDED