@livestore/common 0.5.0-dev.0 → 0.5.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
- package/dist/MaterializationJournal.d.ts +2 -2
- package/dist/MaterializationJournal.d.ts.map +1 -1
- package/dist/MaterializationJournal.js +7 -7
- package/dist/StateHead.d.ts +1 -1
- package/dist/StateHead.d.ts.map +1 -1
- package/dist/StateHead.js +4 -4
- package/dist/WorkerTransportError.d.ts.map +1 -1
- package/dist/__tests__/fixture.d.ts +7 -7
- package/dist/__tests__/fixture.js +2 -2
- package/dist/adapter-types.d.ts +4 -0
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +4 -4
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/bounded-collections.js.map +1 -1
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/debug-info.js +9 -8
- package/dist/debug-info.js.map +1 -1
- package/dist/defs.d.ts +4 -4
- package/dist/defs.js +2 -2
- package/dist/devtools/devtools-compatibility.test.js +1 -1
- package/dist/devtools/devtools-messages-client-session.d.ts +4 -4
- package/dist/devtools/devtools-messages-client-session.js +4 -4
- package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.js +1 -1
- package/dist/devtools/devtools-messages-leader.d.ts +9 -9
- package/dist/devtools/devtools-messages-leader.js +4 -4
- package/dist/devtools/devtools-messages.js +3 -3
- package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
- package/dist/devtools/mod.d.ts.map +1 -1
- package/dist/devtools/mod.js +3 -3
- package/dist/errors.d.ts +4 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js +22 -22
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +27 -6
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +65 -55
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/RejectedPushError.d.ts +9 -9
- package/dist/leader-thread/RejectedPushError.d.ts.map +1 -1
- package/dist/leader-thread/RejectedPushError.js +1 -1
- package/dist/leader-thread/connection.d.ts.map +1 -1
- package/dist/leader-thread/connection.js +2 -2
- package/dist/leader-thread/eventlog.d.ts +11 -1
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +13 -13
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +4 -4
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +6 -0
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +43 -29
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.test.js +2 -2
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +9 -9
- package/dist/leader-thread/mod.js +9 -9
- package/dist/leader-thread/recreate-db.d.ts +5 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +21 -25
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/shutdown-channel.js +1 -1
- package/dist/leader-thread/stream-events.d.ts.map +1 -1
- package/dist/leader-thread/stream-events.js +3 -5
- package/dist/leader-thread/stream-events.js.map +1 -1
- package/dist/leader-thread/types.d.ts +6 -4
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/leader-thread/types.js +5 -1
- package/dist/leader-thread/types.js.map +1 -1
- package/dist/make-client-session.d.ts +1 -1
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +2 -2
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +4 -4
- package/dist/materializer-helper.js.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +3 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +16 -9
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef/define.d.ts +2 -2
- package/dist/schema/EventDef/define.d.ts.map +1 -1
- package/dist/schema/EventDef/define.test.js +1 -1
- package/dist/schema/EventDef/deprecated.d.ts +1 -1
- package/dist/schema/EventDef/deprecated.d.ts.map +1 -1
- package/dist/schema/EventDef/deprecated.test.js +2 -2
- package/dist/schema/EventDef/facts.d.ts.map +1 -1
- package/dist/schema/EventDef/materializer.d.ts +2 -2
- package/dist/schema/EventDef/materializer.d.ts.map +1 -1
- package/dist/schema/EventDef/mod.js +5 -5
- package/dist/schema/EventSequenceNumber/client.d.ts +3 -8
- package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber/client.js +12 -15
- package/dist/schema/EventSequenceNumber/client.js.map +1 -1
- package/dist/schema/EventSequenceNumber/mod.js +2 -2
- package/dist/schema/EventSequenceNumber.test.js +1 -1
- package/dist/schema/LiveStoreEvent/client.d.ts +8 -8
- package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/client.js +1 -1
- package/dist/schema/LiveStoreEvent/client.js.map +1 -1
- package/dist/schema/LiveStoreEvent/client.test.js +2 -2
- package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/global.js +1 -1
- package/dist/schema/LiveStoreEvent/input.d.ts +1 -1
- package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent/mod.js +4 -4
- package/dist/schema/events.js +1 -1
- package/dist/schema/mod.js +11 -11
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +3 -3
- package/dist/schema/state/mod.js +2 -2
- package/dist/schema/state/sqlite/client-document-def.d.ts +1 -1
- package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.js +8 -8
- package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
- package/dist/schema/state/sqlite/client-document-def.test.js +24 -10
- package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-annotations.test.js +1 -1
- package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-def.js +17 -3
- package/dist/schema/state/sqlite/column-def.js.map +1 -1
- package/dist/schema/state/sqlite/column-def.test.js +60 -5
- package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
- package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -1
- package/dist/schema/state/sqlite/column-spec.js +1 -1
- package/dist/schema/state/sqlite/column-spec.test.js +2 -2
- package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js +5 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts +5 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js +183 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts +2 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js +142 -0
- package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -6
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -82
- package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -1
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts +3 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts.map +1 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js +6 -0
- package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js.map +1 -0
- package/dist/schema/state/sqlite/db-schema/mod.js +2 -2
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/mod.js +10 -9
- package/dist/schema/state/sqlite/mod.js.map +1 -1
- package/dist/schema/state/sqlite/persistence.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/astToSql.js +2 -7
- package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts +1 -3
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +2 -2
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +2 -2
- package/dist/schema/state/sqlite/query-builder/mod.js +2 -2
- package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
- package/dist/schema/state/sqlite/schema-helpers.js +1 -1
- package/dist/schema/state/sqlite/schema-helpers.test.js +2 -2
- package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +3 -3
- package/dist/schema/state/sqlite/system-tables/mod.js +2 -2
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +60 -17
- package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables/state-tables.js +14 -5
- package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema/state/sqlite/table-def.js +24 -8
- package/dist/schema/state/sqlite/table-def.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.test.js +54 -1
- package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
- package/dist/schema/unknown-events.d.ts.map +1 -1
- package/dist/schema/unknown-events.js +1 -1
- package/dist/schema/unknown-events.js.map +1 -1
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +26 -4
- package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -1
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/common.js +2 -2
- package/dist/schema-management/migrations.d.ts +4 -4
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +9 -9
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-schema.d.ts.map +1 -1
- package/dist/schema-management/validate-schema.js +1 -1
- package/dist/session-id-symbol.d.ts.map +1 -1
- package/dist/sql-queries/index.js +3 -3
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +3 -3
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
- package/dist/sql-queries/sql-query-builder.js +1 -1
- package/dist/sql-queries/types.d.ts.map +1 -1
- package/dist/sqlite-db-helper.d.ts +2 -2
- package/dist/sqlite-db-helper.d.ts.map +1 -1
- package/dist/sqlite-db-helper.js +2 -2
- package/dist/sync/ClientSessionSyncProcessor.d.ts +3 -3
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +7 -7
- package/dist/sync/errors.js +1 -1
- package/dist/sync/index.js +6 -6
- package/dist/sync/mock-sync-backend.d.ts.map +1 -1
- package/dist/sync/mock-sync-backend.js +4 -4
- package/dist/sync/mock-sync-backend.test.js +2 -2
- package/dist/sync/next/compact-events.d.ts.map +1 -1
- package/dist/sync/next/compact-events.js +3 -3
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +1 -1
- package/dist/sync/next/history-dag-common.d.ts +1 -1
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +1 -1
- package/dist/sync/next/history-dag.d.ts.map +1 -1
- package/dist/sync/next/history-dag.js +3 -3
- package/dist/sync/next/mod.js +5 -5
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +2 -2
- package/dist/sync/next/test/compact-events.calculator.test.js +4 -4
- package/dist/sync/next/test/compact-events.test.js +4 -4
- package/dist/sync/next/test/event-fixtures.d.ts +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +5 -5
- package/dist/sync/next/test/history-dag-common.test.js +2 -2
- package/dist/sync/next/test/mod.js +1 -1
- package/dist/sync/sync-backend-kv.js +1 -1
- package/dist/sync/sync-backend.d.ts.map +1 -1
- package/dist/sync/sync-backend.js +1 -1
- package/dist/sync/sync.js +2 -2
- package/dist/sync/syncstate.d.ts +3 -3
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +2 -2
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +3 -3
- package/dist/sync/transport-chunking.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js +2 -2
- package/dist/testing/event-factory.d.ts.map +1 -1
- package/dist/testing/event-factory.js +2 -2
- package/dist/testing/event-factory.js.map +1 -1
- package/dist/testing/mod.js +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +40 -39
- package/src/adapter-types.ts +4 -0
- package/src/debug-info.ts +11 -8
- package/src/defs.ts +2 -2
- package/src/leader-thread/LeaderSyncProcessor.ts +121 -97
- package/src/leader-thread/eventlog.ts +13 -5
- package/src/leader-thread/leader-worker-devtools.ts +1 -1
- package/src/leader-thread/make-leader-thread-layer.ts +49 -23
- package/src/leader-thread/recreate-db.ts +26 -25
- package/src/leader-thread/stream-events.ts +1 -3
- package/src/leader-thread/types.ts +5 -0
- package/src/materializer-helper.ts +1 -1
- package/src/rematerialize-from-eventlog.ts +28 -13
- package/src/schema/EventSequenceNumber/client.ts +12 -15
- package/src/schema/schema.ts +1 -1
- package/src/schema/state/sqlite/client-document-def.test.ts +23 -7
- package/src/schema/state/sqlite/client-document-def.ts +2 -2
- package/src/schema/state/sqlite/column-def.test.ts +84 -3
- package/src/schema/state/sqlite/column-def.ts +16 -1
- package/src/schema/state/sqlite/db-schema/ast/fingerprint-digest.ts +6 -0
- package/src/schema/state/sqlite/db-schema/ast/fingerprint.test.ts +209 -0
- package/src/schema/state/sqlite/db-schema/ast/fingerprint.ts +225 -0
- package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +1 -90
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +1 -0
- package/src/schema/state/sqlite/db-schema/has-json-string-encoding.ts +9 -0
- package/src/schema/state/sqlite/mod.ts +2 -1
- package/src/schema/state/sqlite/query-builder/astToSql.ts +1 -8
- package/src/schema/state/sqlite/system-tables/state-tables.ts +13 -2
- package/src/schema/state/sqlite/table-def.test.ts +66 -0
- package/src/schema/state/sqlite/table-def.ts +21 -5
- package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +33 -2
- package/src/schema-management/migrations.ts +7 -6
- package/src/sql-queries/sql-queries.ts +1 -1
- package/src/util.ts +1 -0
- package/dist/schema/state/sqlite/db-schema/hash.d.ts +0 -2
- package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +0 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +0 -15
- package/dist/schema/state/sqlite/db-schema/hash.js.map +0 -1
- package/src/schema/state/sqlite/db-schema/hash.ts +0 -13
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Schema } from '@livestore/utils/effect'
|
|
4
|
+
|
|
5
|
+
import { liveStoreStorageFormatVersion } from '../../../../../version.ts'
|
|
6
|
+
import { makeState } from '../../mod.ts'
|
|
7
|
+
import { REBUILD_META_TABLE } from '../../system-tables/state-tables.ts'
|
|
8
|
+
import { SqliteDsl } from '../mod.ts'
|
|
9
|
+
import { digestToFingerprint } from './fingerprint-digest.ts'
|
|
10
|
+
import { fingerprint } from './fingerprint.ts'
|
|
11
|
+
|
|
12
|
+
describe('SQLite storage fingerprints', () => {
|
|
13
|
+
test('pins the final fingerprint output', () => {
|
|
14
|
+
expect(fingerprint(makePhysicalTable().ast)).toBe('UnLwYzVBhwzPCK5q7TrPU3q2N0dTe8m_98bud8HsAs8')
|
|
15
|
+
expect(fingerprint(makeJsonTable('documents', representativeJsonSchema).ast)).toBe(
|
|
16
|
+
'kUzaurzV2rcXYLljHOZ64TDR9c_RebqD7y5ZUM_nPBE',
|
|
17
|
+
)
|
|
18
|
+
expect(fingerprint(makeJsonTable('documents', normalizedCheckIdsJsonSchema).ast)).toBe(
|
|
19
|
+
'I0XYuOUSYqWwG5Opxbc1u-TNNE3pFQCHzpBZ_ChYGaA',
|
|
20
|
+
)
|
|
21
|
+
expect(makeState({ tables: [], materializers: {} }).sqlite.hash).toBe('o8mmnRDnhXhkgDpBybM3FK7S-PkaLEj_pKcnbzgg3TU')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('isolates pre-completion-marker state databases without changing the hash algorithm', () => {
|
|
25
|
+
const state = makeState({ tables: [], materializers: {} })
|
|
26
|
+
const legacyTables = [...state.sqlite.tables.values()]
|
|
27
|
+
.filter((table) => table.sqliteDef.name !== REBUILD_META_TABLE)
|
|
28
|
+
.map((table) => table.sqliteDef.ast)
|
|
29
|
+
const legacyHash = fingerprint({ _tag: 'dbSchema', tables: legacyTables })
|
|
30
|
+
expect(legacyHash).toBe('H5Uktp6Ffp84WDj_RWPnfDnaQEu_E61AMl7ieZ9Zn8k')
|
|
31
|
+
expect(state.sqlite.hash).not.toBe(legacyHash)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('matches the standard SHA-256 vector', () => {
|
|
35
|
+
expect(digestToFingerprint(new TextEncoder().encode('abc'))).toBe('ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('keeps the full digest inside the Cloudflare VFS filename limit', () => {
|
|
39
|
+
const value = fingerprint(makePhysicalTable().ast)
|
|
40
|
+
|
|
41
|
+
expect(value).toHaveLength(43)
|
|
42
|
+
expect(`state${value}@${liveStoreStorageFormatVersion}.db`.length).toBeLessThanOrEqual(56)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('is independent of table and index declaration order', () => {
|
|
46
|
+
const first = makeJsonTable('first', Schema.Struct({ value: Schema.String }))
|
|
47
|
+
const second = makeJsonTable('second', Schema.Struct({ value: Schema.Finite }))
|
|
48
|
+
|
|
49
|
+
expect(fingerprint({ _tag: 'dbSchema', tables: [first.ast, second.ast] })).toBe(
|
|
50
|
+
fingerprint({ _tag: 'dbSchema', tables: [second.ast, first.ast] }),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const table = SqliteDsl.table(
|
|
54
|
+
'indexed',
|
|
55
|
+
{
|
|
56
|
+
id: SqliteDsl.text({ primaryKey: true }),
|
|
57
|
+
age: SqliteDsl.integer(),
|
|
58
|
+
},
|
|
59
|
+
[
|
|
60
|
+
{ name: 'indexed_by_age', columns: ['age'], isUnique: false },
|
|
61
|
+
{ name: 'indexed_by_id', columns: ['id'], isUnique: true },
|
|
62
|
+
],
|
|
63
|
+
).ast
|
|
64
|
+
expect(fingerprint({ ...table, indexes: table.indexes.toReversed() })).toBe(fingerprint(table))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('ignores Effect metadata that does not affect accepted encoded JSON', () => {
|
|
68
|
+
const plain = makeJsonTable('documents', Schema.Struct({ value: Schema.String }))
|
|
69
|
+
const annotated = makeJsonTable(
|
|
70
|
+
'documents',
|
|
71
|
+
Schema.Struct({ value: Schema.String }).annotate({ description: 'documentation only', title: 'Document' }),
|
|
72
|
+
)
|
|
73
|
+
const readonlyArray = makeJsonTable('documents', Schema.Array(Schema.String))
|
|
74
|
+
const mutableArray = makeJsonTable('documents', Schema.mutable(Schema.Array(Schema.String)))
|
|
75
|
+
|
|
76
|
+
expect(fingerprint(plain.ast)).toBe(fingerprint(annotated.ast))
|
|
77
|
+
expect(fingerprint(readonlyArray.ast)).toBe(fingerprint(mutableArray.ast))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('changes when represented JSON checks change', () => {
|
|
81
|
+
const minimumTwo = makeJsonTable('documents', Schema.Struct({ value: Schema.String.check(Schema.isMinLength(2)) }))
|
|
82
|
+
const minimumThree = makeJsonTable(
|
|
83
|
+
'documents',
|
|
84
|
+
Schema.Struct({ value: Schema.String.check(Schema.isMinLength(3)) }),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
expect(fingerprint(minimumTwo.ast)).not.toBe(fingerprint(minimumThree.ast))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('distinguishes codecs with the same encoded primitive', () => {
|
|
91
|
+
const date = makeJsonTable('documents', Schema.Struct({ value: Schema.DateFromString }))
|
|
92
|
+
const string = makeJsonTable('documents', Schema.Struct({ value: Schema.String }))
|
|
93
|
+
|
|
94
|
+
expect(fingerprint(date.ast)).not.toBe(fingerprint(string.ast))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('handles recursive JSON schemas deterministically', () => {
|
|
98
|
+
const first = makeJsonTable('trees', makeTreeSchema())
|
|
99
|
+
const second = makeJsonTable('trees', makeTreeSchema())
|
|
100
|
+
|
|
101
|
+
expect(fingerprint(first.ast)).toBe(fingerprint(second.ast))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('supports opaque Effect schemas without persistence annotations', () => {
|
|
105
|
+
const OpaqueString = Schema.declare<string>((value): value is string => typeof value === 'string')
|
|
106
|
+
const table = makeJsonTable('opaque_documents', Schema.Struct({ value: OpaqueString }))
|
|
107
|
+
|
|
108
|
+
expect(() => fingerprint(table.ast)).not.toThrow()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('keeps application-owned representation payload fields', () => {
|
|
112
|
+
const first = Schema.declare<string>((value): value is string => typeof value === 'string', {
|
|
113
|
+
representation: { id: 'example/opaque', payload: { annotations: 'first', isMutable: true } },
|
|
114
|
+
})
|
|
115
|
+
const second = Schema.declare<string>((value): value is string => typeof value === 'string', {
|
|
116
|
+
representation: { id: 'example/opaque', payload: { annotations: 'second', isMutable: true } },
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(fingerprint(makeJsonTable('documents', first).ast)).not.toBe(
|
|
120
|
+
fingerprint(makeJsonTable('documents', second).ast),
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('tracks JSON codecs passed through the generic text column API', () => {
|
|
125
|
+
const first = SqliteDsl.table('documents', {
|
|
126
|
+
value: SqliteDsl.text({ schema: Schema.fromJsonString(Schema.Struct({ value: Schema.String })) }),
|
|
127
|
+
})
|
|
128
|
+
const second = SqliteDsl.table('documents', {
|
|
129
|
+
value: SqliteDsl.text({ schema: Schema.fromJsonString(Schema.Struct({ value: Schema.Finite })) }),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(fingerprint(first.ast)).not.toBe(fingerprint(second.ast))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('gives equivalent JSON DSL forms the same fingerprint', () => {
|
|
136
|
+
const valueSchema = Schema.Struct({ value: Schema.String })
|
|
137
|
+
const specialized = SqliteDsl.table('documents', {
|
|
138
|
+
value: SqliteDsl.json({ schema: valueSchema }),
|
|
139
|
+
})
|
|
140
|
+
const generic = SqliteDsl.table('documents', {
|
|
141
|
+
value: SqliteDsl.text({ schema: Schema.fromJsonString(valueSchema) }),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(fingerprint(specialized.ast)).toBe(fingerprint(generic.ast))
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('classifies physical defaults without hashing function source text', () => {
|
|
148
|
+
const none = SqliteDsl.text()
|
|
149
|
+
const firstThunk = SqliteDsl.text({ default: () => 'first' })
|
|
150
|
+
const secondThunk = SqliteDsl.text({ default: () => 'second' })
|
|
151
|
+
const literal = SqliteDsl.text({ default: 'first' })
|
|
152
|
+
const toTable = (definition: SqliteDsl.ColumnDefinition.Any) =>
|
|
153
|
+
SqliteDsl.table('defaults', { value: definition }).ast
|
|
154
|
+
|
|
155
|
+
expect(fingerprint(toTable(firstThunk))).toBe(fingerprint(toTable(secondThunk)))
|
|
156
|
+
expect(fingerprint(toTable(firstThunk))).not.toBe(fingerprint(toTable(none)))
|
|
157
|
+
expect(fingerprint(toTable(literal))).not.toBe(fingerprint(toTable(firstThunk)))
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const makePhysicalTable = () =>
|
|
162
|
+
SqliteDsl.table(
|
|
163
|
+
'users',
|
|
164
|
+
{
|
|
165
|
+
id: SqliteDsl.text({ primaryKey: true }),
|
|
166
|
+
age: SqliteDsl.integer({ nullable: true }),
|
|
167
|
+
},
|
|
168
|
+
[{ name: 'users_by_age', columns: ['age'], isUnique: false }],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const makeJsonTable = (name: string, schema: Schema.Codec<unknown, unknown>) =>
|
|
172
|
+
SqliteDsl.table(name, {
|
|
173
|
+
id: SqliteDsl.text({ primaryKey: true }),
|
|
174
|
+
value: SqliteDsl.json({ schema }),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const representativeJsonSchema = Schema.Struct({
|
|
178
|
+
title: Schema.String.check(Schema.isMinLength(1)),
|
|
179
|
+
tags: Schema.Array(Schema.String),
|
|
180
|
+
status: Schema.Union([Schema.Literal('draft'), Schema.Literal('published')]),
|
|
181
|
+
publishedAt: Schema.NullOr(Schema.DateFromString),
|
|
182
|
+
metadata: Schema.Record(Schema.String, Schema.Json),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
/** Uses the JSON-compatible checks whose representation ids the fingerprint normalizes. */
|
|
186
|
+
const normalizedCheckIdsJsonSchema = Schema.Struct({
|
|
187
|
+
slug: Schema.String.check(
|
|
188
|
+
Schema.isBetweenLength(1, 64),
|
|
189
|
+
Schema.isStartingWith('a'),
|
|
190
|
+
Schema.isEndingWith('z'),
|
|
191
|
+
Schema.isIncluding('-'),
|
|
192
|
+
),
|
|
193
|
+
tags: Schema.Array(Schema.String).check(Schema.isBetweenLength(0, 10)),
|
|
194
|
+
initial: Schema.Char,
|
|
195
|
+
labels: Schema.Record(Schema.String, Schema.String).check(Schema.isBetweenProperties(0, 5)),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
interface TreeNode {
|
|
199
|
+
readonly value: string
|
|
200
|
+
readonly children: ReadonlyArray<TreeNode>
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const makeTreeSchema = (): Schema.Codec<TreeNode> => {
|
|
204
|
+
const TreeNode: Schema.Codec<TreeNode> = Schema.Struct({
|
|
205
|
+
value: Schema.String,
|
|
206
|
+
children: Schema.Array(Schema.suspend(() => TreeNode)),
|
|
207
|
+
})
|
|
208
|
+
return TreeNode
|
|
209
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { casesHandled, textEncodeToArrayBuffer } from '@livestore/utils'
|
|
2
|
+
import { Schema, SchemaAST, SchemaRepresentation } from '@livestore/utils/effect'
|
|
3
|
+
|
|
4
|
+
import { isDefaultThunk, isSqlDefaultValue } from '../dsl/field-defs.ts'
|
|
5
|
+
import { hasJsonStringEncoding } from '../has-json-string-encoding.ts'
|
|
6
|
+
import { digestToFingerprint } from './fingerprint-digest.ts'
|
|
7
|
+
import type { Column, ColumnType, DbSchema, Index, Table } from './sqlite.ts'
|
|
8
|
+
|
|
9
|
+
type FingerprintInput = Table | DbSchema
|
|
10
|
+
|
|
11
|
+
export const fingerprint = (input: FingerprintInput): string => digestToFingerprint(toCanonicalBytes(input))
|
|
12
|
+
|
|
13
|
+
type CanonicalValue = Schema.Json
|
|
14
|
+
|
|
15
|
+
type SchemaDescriptorCache = WeakMap<Schema.Top, CanonicalValue>
|
|
16
|
+
|
|
17
|
+
const domain = 'livestore/state-storage-fingerprint'
|
|
18
|
+
|
|
19
|
+
const toCanonicalBytes = (input: FingerprintInput): Uint8Array<ArrayBuffer> =>
|
|
20
|
+
textEncodeToArrayBuffer(JSON.stringify(toRootDescriptor(input)))
|
|
21
|
+
|
|
22
|
+
const toRootDescriptor = (input: FingerprintInput): CanonicalValue => {
|
|
23
|
+
// Limit schema memoization to one fingerprint operation so descriptors remain collectible.
|
|
24
|
+
const cache: SchemaDescriptorCache = new WeakMap()
|
|
25
|
+
return [domain, toDescriptor(input, cache)]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const toDescriptor = (input: FingerprintInput, cache: SchemaDescriptorCache): CanonicalValue => {
|
|
29
|
+
switch (input._tag) {
|
|
30
|
+
case 'table':
|
|
31
|
+
return tableDescriptor(input, cache)
|
|
32
|
+
case 'dbSchema':
|
|
33
|
+
return dbSchemaDescriptor(input, cache)
|
|
34
|
+
default:
|
|
35
|
+
return casesHandled(input)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const columnDescriptor = (column: Column, cache: SchemaDescriptorCache): CanonicalValue => {
|
|
40
|
+
const {
|
|
41
|
+
_tag: _tagIgnored,
|
|
42
|
+
name,
|
|
43
|
+
type,
|
|
44
|
+
primaryKey,
|
|
45
|
+
nullable,
|
|
46
|
+
autoIncrement,
|
|
47
|
+
default: defaultValue,
|
|
48
|
+
schema,
|
|
49
|
+
...unhandled
|
|
50
|
+
} = column
|
|
51
|
+
assertNoUnhandledKeys(unhandled)
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
'column',
|
|
55
|
+
name,
|
|
56
|
+
columnTypeDescriptor(type),
|
|
57
|
+
primaryKey,
|
|
58
|
+
nullable,
|
|
59
|
+
autoIncrement,
|
|
60
|
+
defaultDescriptor(defaultValue, schema),
|
|
61
|
+
hasJsonStringEncoding(schema.ast) === true ? schemaDescriptor(schema, cache) : null,
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const columnTypeDescriptor = (type: ColumnType.ColumnType): string => {
|
|
66
|
+
const { _tag, ...unhandled } = type
|
|
67
|
+
assertNoUnhandledKeys(unhandled)
|
|
68
|
+
switch (_tag) {
|
|
69
|
+
case 'text':
|
|
70
|
+
case 'null':
|
|
71
|
+
case 'real':
|
|
72
|
+
case 'integer':
|
|
73
|
+
case 'blob':
|
|
74
|
+
return _tag
|
|
75
|
+
default:
|
|
76
|
+
return casesHandled(_tag)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const indexDescriptor = (index: Index): CanonicalValue => {
|
|
81
|
+
const { _tag: _tagIgnored, name, unique, primaryKey, columns, ...unhandled } = index
|
|
82
|
+
assertNoUnhandledKeys(unhandled)
|
|
83
|
+
return ['index', name ?? null, unique ?? false, primaryKey ?? false, columns]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const tableDescriptor = (table: Table, cache: SchemaDescriptorCache): CanonicalValue => {
|
|
87
|
+
const { _tag: _tagIgnored, name, columns, indexes, ...unhandled } = table
|
|
88
|
+
assertNoUnhandledKeys(unhandled)
|
|
89
|
+
return [
|
|
90
|
+
'table',
|
|
91
|
+
name,
|
|
92
|
+
columns.map((column) => columnDescriptor(column, cache)),
|
|
93
|
+
sortCanonicalValues(indexes.map(indexDescriptor)),
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dbSchemaDescriptor = (dbSchema: DbSchema, cache: SchemaDescriptorCache): CanonicalValue => {
|
|
98
|
+
const { _tag: _tagIgnored, tables, ...unhandled } = dbSchema
|
|
99
|
+
assertNoUnhandledKeys(unhandled)
|
|
100
|
+
return ['database', sortByCanonicalKey(tables, (table) => table.name).map((table) => tableDescriptor(table, cache))]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const defaultDescriptor = (value: Column['default'], schema: Column['schema']): CanonicalValue => {
|
|
104
|
+
if (value._tag === 'None') return ['none']
|
|
105
|
+
if (isDefaultThunk(value.value) === true) return ['runtime-default']
|
|
106
|
+
if (value.value === null) return ['value', null]
|
|
107
|
+
if (isSqlDefaultValue(value.value) === true) return ['sql', value.value.sql]
|
|
108
|
+
|
|
109
|
+
return ['value', canonicalValue(Schema.encodeSync(schema)(value.value))]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const schemaDescriptor = (schema: Schema.Top, cache: SchemaDescriptorCache): CanonicalValue => {
|
|
113
|
+
const cached = cache.get(schema)
|
|
114
|
+
if (cached !== undefined) return cached
|
|
115
|
+
|
|
116
|
+
// Both ends matter: the encoded side describes stored JSON while the type side
|
|
117
|
+
// distinguishes codecs such as DateFromString from an unconstrained string.
|
|
118
|
+
const document = SchemaRepresentation.toRepresentations([schema.ast, SchemaAST.toType(schema.ast)], {
|
|
119
|
+
referencePolicy: () => undefined,
|
|
120
|
+
})
|
|
121
|
+
const descriptor: CanonicalValue = ['schema', schemaRepresentationDescriptor(document)]
|
|
122
|
+
cache.set(schema, descriptor)
|
|
123
|
+
return descriptor
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const schemaRepresentationDescriptor = (value: unknown, parentKey?: string): CanonicalValue => {
|
|
127
|
+
// Representation payloads are application-owned JSON. Keys such as `annotations`
|
|
128
|
+
// inside them are data, not Effect metadata.
|
|
129
|
+
if (parentKey === 'payload') return canonicalValue(value)
|
|
130
|
+
|
|
131
|
+
if (Array.isArray(value) === true) return value.map((item) => schemaRepresentationDescriptor(item))
|
|
132
|
+
|
|
133
|
+
if (value !== null && typeof value === 'object' && value instanceof Uint8Array === false) {
|
|
134
|
+
const sourceEntries = Object.entries(value).map(([key, entryValue]): [string, unknown] =>
|
|
135
|
+
parentKey === 'representation' && key === 'id' && typeof entryValue === 'string'
|
|
136
|
+
? [key, renamedEffectCheckIds.get(entryValue) ?? entryValue]
|
|
137
|
+
: [key, entryValue],
|
|
138
|
+
)
|
|
139
|
+
const isUnion = sourceEntries.some(([key, entryValue]) => key === '_tag' && entryValue === 'Union')
|
|
140
|
+
// Effect rc.113 removed this representation field because unions only support `anyOf`.
|
|
141
|
+
// Restore the old default so dependency upgrades preserve existing state fingerprints.
|
|
142
|
+
if (isUnion === true && sourceEntries.some(([key]) => key === 'mode') === false) {
|
|
143
|
+
sourceEntries.push(['mode', 'anyOf'])
|
|
144
|
+
}
|
|
145
|
+
const entries = sortByCanonicalKey(
|
|
146
|
+
sourceEntries.filter(([key]) => effectIgnoredFields.has(key) === false),
|
|
147
|
+
([key]) => key,
|
|
148
|
+
).map(([key, entryValue]) => [key, schemaRepresentationDescriptor(entryValue, key)] as const)
|
|
149
|
+
return Object.fromEntries(entries)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return canonicalValue(value)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const effectIgnoredFields = new Set(['annotations', 'isMutable'])
|
|
156
|
+
|
|
157
|
+
/** Maps renamed Effect check ids to the ids existing fingerprints were computed from, so the rename does not rebuild state. */
|
|
158
|
+
const renamedEffectCheckIds = new Map([
|
|
159
|
+
['effect/schema/isBetweenLength', 'effect/schema/isLengthBetween'],
|
|
160
|
+
['effect/schema/isStartingWith', 'effect/schema/isStartsWith'],
|
|
161
|
+
['effect/schema/isEndingWith', 'effect/schema/isEndsWith'],
|
|
162
|
+
['effect/schema/isIncluding', 'effect/schema/isIncludes'],
|
|
163
|
+
['effect/schema/isBetweenSize', 'effect/schema/isSizeBetween'],
|
|
164
|
+
['effect/schema/isBetweenProperties', 'effect/schema/isPropertiesLengthBetween'],
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
const canonicalValue = (value: unknown): CanonicalValue => {
|
|
168
|
+
if (Array.isArray(value) === true) return value.map(canonicalValue)
|
|
169
|
+
|
|
170
|
+
if (value !== null && typeof value === 'object' && value instanceof Uint8Array === false) {
|
|
171
|
+
const entries = sortByCanonicalKey(Object.entries(value), ([key]) => key).map(
|
|
172
|
+
([key, entryValue]) => [key, canonicalValue(entryValue)] as const,
|
|
173
|
+
)
|
|
174
|
+
return Object.fromEntries(entries)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value
|
|
178
|
+
|
|
179
|
+
if (typeof value === 'number') {
|
|
180
|
+
if (Number.isNaN(value) === true) return specialValue('number', 'NaN')
|
|
181
|
+
if (value === Number.POSITIVE_INFINITY) return specialValue('number', 'Infinity')
|
|
182
|
+
if (value === Number.NEGATIVE_INFINITY) return specialValue('number', '-Infinity')
|
|
183
|
+
if (Object.is(value, -0) === true) return specialValue('number', '-0')
|
|
184
|
+
return value
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (typeof value === 'bigint') return specialValue('bigint', value.toString(10))
|
|
188
|
+
if (value === undefined) return specialValue('undefined')
|
|
189
|
+
|
|
190
|
+
if (typeof value === 'symbol') {
|
|
191
|
+
const symbolKey = Symbol.keyFor(value)
|
|
192
|
+
if (symbolKey === undefined) throw new Error('Local symbols cannot be represented in a persistent fingerprint')
|
|
193
|
+
return specialValue('symbol', symbolKey)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof value === 'function') throw new Error('Functions cannot be represented in a persistent fingerprint')
|
|
197
|
+
if (value instanceof Uint8Array) return specialValue('bytes', bytesToHex(value))
|
|
198
|
+
|
|
199
|
+
throw new Error('Unsupported canonical value')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const specialValue = (type: string, value?: string): CanonicalValue => ({
|
|
203
|
+
$livestore$type: value === undefined ? [type] : [type, value],
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const bytesToHex = (bytes: Uint8Array): string => {
|
|
207
|
+
let output = ''
|
|
208
|
+
for (const byte of bytes) output += byte.toString(16).padStart(2, '0')
|
|
209
|
+
return output
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Avoid repeatedly serializing deep descriptors from inside an O(n log n) sort comparator. */
|
|
213
|
+
const sortCanonicalValues = (values: ReadonlyArray<CanonicalValue>): ReadonlyArray<CanonicalValue> =>
|
|
214
|
+
values
|
|
215
|
+
.map((value) => ({ value, key: JSON.stringify(value) }))
|
|
216
|
+
.toSorted((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0))
|
|
217
|
+
.map(({ value }) => value)
|
|
218
|
+
|
|
219
|
+
const sortByCanonicalKey = <A>(values: ReadonlyArray<A>, key: (value: A) => CanonicalValue): ReadonlyArray<A> =>
|
|
220
|
+
values
|
|
221
|
+
.map((value) => ({ value, key: JSON.stringify(key(value)) }))
|
|
222
|
+
.toSorted((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0))
|
|
223
|
+
.map(({ value }) => value)
|
|
224
|
+
|
|
225
|
+
const assertNoUnhandledKeys = (_unhandled: Record<PropertyKey, never>): void => {}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { omitUndefineds } from '@livestore/utils'
|
|
2
|
-
import { type Option, Schema
|
|
3
|
-
|
|
4
|
-
import { hashCode } from '../hash.ts'
|
|
2
|
+
import { type Option, Schema } from '@livestore/utils/effect'
|
|
5
3
|
|
|
6
4
|
export namespace ColumnType {
|
|
7
5
|
export type ColumnType = Text | Null | Real | Integer | Blob
|
|
@@ -83,92 +81,5 @@ export type DbSchema = {
|
|
|
83
81
|
|
|
84
82
|
export const dbSchema = (tables: Table[]): DbSchema => ({ _tag: 'dbSchema', tables })
|
|
85
83
|
|
|
86
|
-
/**
|
|
87
|
-
* Helper to detect if a column is a JSON column (encoded as a JSON string)
|
|
88
|
-
*/
|
|
89
|
-
const isJsonColumn = (column: Column): boolean => {
|
|
90
|
-
if (column.type._tag !== 'text') return false
|
|
91
|
-
|
|
92
|
-
return hasJsonStringEncoding(column.schema.ast)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* `SchemaTransformation.fromJsonString` is a factory since Effect rc.109, so encoding links can no
|
|
97
|
-
* longer be matched by identity. `Schema.fromJsonString` annotates its encoded side as
|
|
98
|
-
* `application/json`, which identifies the link regardless of the reviver/replacer/space options.
|
|
99
|
-
*/
|
|
100
|
-
const hasJsonStringEncoding = (ast: SchemaAST.AST): boolean => {
|
|
101
|
-
if (ast.encoding?.some((link) => link.to.annotations?.contentMediaType === 'application/json') === true) {
|
|
102
|
-
return true
|
|
103
|
-
}
|
|
104
|
-
return SchemaAST.isUnion(ast) === true && ast.types.some(hasJsonStringEncoding)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* NOTE we're now including JSON schema information for JSON columns
|
|
109
|
-
* to detect client document schema changes
|
|
110
|
-
*/
|
|
111
|
-
export const hash = (obj: Table | Column | Index | ForeignKey | DbSchema): number =>
|
|
112
|
-
hashCode(JSON.stringify(trimInfoForHasing(obj)))
|
|
113
|
-
|
|
114
|
-
const trimInfoForHasing = (obj: Table | Column | Index | ForeignKey | DbSchema): Record<string, any> => {
|
|
115
|
-
switch (obj._tag) {
|
|
116
|
-
case 'table': {
|
|
117
|
-
return {
|
|
118
|
-
_tag: 'table',
|
|
119
|
-
name: obj.name,
|
|
120
|
-
columns: obj.columns.map((column) => trimInfoForHasing(column)),
|
|
121
|
-
indexes: obj.indexes.map((index) => trimInfoForHasing(index)),
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
case 'column': {
|
|
125
|
-
const baseInfo: Record<string, any> = {
|
|
126
|
-
_tag: 'column',
|
|
127
|
-
name: obj.name,
|
|
128
|
-
type: obj.type._tag,
|
|
129
|
-
primaryKey: obj.primaryKey,
|
|
130
|
-
nullable: obj.nullable,
|
|
131
|
-
autoIncrement: obj.autoIncrement,
|
|
132
|
-
default: obj.default,
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// NEW: Include schema hash for JSON columns
|
|
136
|
-
// This ensures that changes to the JSON schema are detected
|
|
137
|
-
if (isJsonColumn(obj) === true && obj.schema !== undefined) {
|
|
138
|
-
// Use Effect's Schema.hash for consistent hashing
|
|
139
|
-
baseInfo.jsonSchemaHash = Schema.hash(obj.schema)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return baseInfo
|
|
143
|
-
}
|
|
144
|
-
case 'index': {
|
|
145
|
-
return {
|
|
146
|
-
_tag: 'index',
|
|
147
|
-
columns: obj.columns,
|
|
148
|
-
name: obj.name,
|
|
149
|
-
unique: obj.unique,
|
|
150
|
-
primaryKey: obj.primaryKey,
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
case 'foreignKey': {
|
|
154
|
-
return {
|
|
155
|
-
_tag: 'foreignKey',
|
|
156
|
-
references: obj.references,
|
|
157
|
-
key: obj.key,
|
|
158
|
-
columns: obj.columns,
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
case 'dbSchema': {
|
|
162
|
-
return {
|
|
163
|
-
_tag: 'dbSchema',
|
|
164
|
-
tables: obj.tables.map(trimInfoForHasing),
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
default: {
|
|
168
|
-
throw new Error(`Unreachable: ${String(obj)}`)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
84
|
export const structSchemaForTable = (tableDef: Table) =>
|
|
174
85
|
Schema.Struct(Object.fromEntries(tableDef.columns.map((column) => [column.name, column.schema])))
|
|
@@ -267,6 +267,7 @@ export const defaultSchemaForColumnType = <TColumnType extends FieldColumnType>(
|
|
|
267
267
|
}
|
|
268
268
|
case 'real': {
|
|
269
269
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- switch-based type narrowing for column type to schema mapping; each case is correct for its branch
|
|
270
|
+
// @effect-diagnostics-next-line schemaNumber:off -- SQLite REAL permits Infinity/NaN; SqliteReal is the documented domain carve-out
|
|
270
271
|
return SqliteReal as Schema.Codec<T>
|
|
271
272
|
}
|
|
272
273
|
case 'blob': {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SchemaAST } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
const getContentMediaType = SchemaAST.resolveAt<string>('contentMediaType')
|
|
4
|
+
|
|
5
|
+
export const hasJsonStringEncoding = (ast: SchemaAST.AST): boolean => hasJsonMediaType(SchemaAST.toEncoded(ast))
|
|
6
|
+
|
|
7
|
+
const hasJsonMediaType = (ast: SchemaAST.AST): boolean =>
|
|
8
|
+
getContentMediaType(ast) === 'application/json' ||
|
|
9
|
+
(SchemaAST.isUnion(ast) === true && ast.types.some(hasJsonMediaType))
|
|
@@ -4,6 +4,7 @@ import type { MigrationOptions } from '../../../adapter-types.ts'
|
|
|
4
4
|
import type { Materializer } from '../../EventDef/mod.ts'
|
|
5
5
|
import type { InternalState } from '../../schema.ts'
|
|
6
6
|
import { ClientDocumentTableDefSymbol, tableIsClientDocumentTable } from './client-document-def.ts'
|
|
7
|
+
import { fingerprint } from './db-schema/ast/fingerprint.ts'
|
|
7
8
|
import { SqliteAst } from './db-schema/mod.ts'
|
|
8
9
|
import { stateSystemTables } from './system-tables/state-tables.ts'
|
|
9
10
|
import type { TableDef, TableDefBase } from './table-def.ts'
|
|
@@ -55,7 +56,7 @@ export const makeState = <TStateInput extends InputState>(inputSchema: TStateInp
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
const hash =
|
|
59
|
+
const hash = fingerprint({
|
|
59
60
|
_tag: 'dbSchema',
|
|
60
61
|
tables: [...tables.values()].map((_) => _.sqliteDef.ast),
|
|
61
62
|
})
|
|
@@ -4,6 +4,7 @@ import { Schema, SchemaAST, Struct } from '@livestore/utils/effect'
|
|
|
4
4
|
import { SessionIdSymbol } from '../../../../session-id-symbol.ts'
|
|
5
5
|
import type { SqlValue } from '../../../../util.ts'
|
|
6
6
|
import type { State } from '../../../mod.ts'
|
|
7
|
+
import { hasJsonStringEncoding } from '../db-schema/has-json-string-encoding.ts'
|
|
7
8
|
import type { QueryBuilderAst } from './api.ts'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -47,14 +48,6 @@ const getJsonArrayElementSchema = (colSchema: Schema.Top): Schema.Top | undefine
|
|
|
47
48
|
return undefined
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
* `SchemaTransformation.fromJsonString` is a factory since Effect rc.109, so encoding links can no
|
|
52
|
-
* longer be matched by identity. `Schema.fromJsonString` annotates its encoded side as
|
|
53
|
-
* `application/json`, which identifies the link regardless of the reviver/replacer/space options.
|
|
54
|
-
*/
|
|
55
|
-
const hasJsonStringEncoding = (ast: SchemaAST.AST): boolean =>
|
|
56
|
-
ast.encoding?.some((link) => link.to.annotations?.contentMediaType === 'application/json') === true
|
|
57
|
-
|
|
58
51
|
/**
|
|
59
52
|
* Encodes a JSON array element to the representation returned by SQLite's json_each().
|
|
60
53
|
* Objects/arrays are stringified so they match json_each's TEXT representation.
|
|
@@ -6,7 +6,7 @@ import { table } from '../table-def.ts'
|
|
|
6
6
|
* STATE DATABASE SYSTEM TABLES
|
|
7
7
|
*
|
|
8
8
|
* ⚠️ SAFE TO CHANGE: State tables are automatically rebuilt from eventlog when schema changes.
|
|
9
|
-
* No need to bump `liveStoreStorageFormatVersion`
|
|
9
|
+
* No need to bump `liveStoreStorageFormatVersion` because state tables use fingerprint-based migration.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export const SCHEMA_META_TABLE = '__livestore_schema'
|
|
@@ -18,7 +18,7 @@ export const schemaMetaTable = table({
|
|
|
18
18
|
name: SCHEMA_META_TABLE,
|
|
19
19
|
columns: {
|
|
20
20
|
tableName: SqliteDsl.text({ primaryKey: true }),
|
|
21
|
-
schemaHash: SqliteDsl.
|
|
21
|
+
schemaHash: SqliteDsl.text({ nullable: false }),
|
|
22
22
|
/** ISO date format */
|
|
23
23
|
updatedAt: SqliteDsl.text({ nullable: false }),
|
|
24
24
|
},
|
|
@@ -64,6 +64,16 @@ export const stateHeadMetaTable = table({
|
|
|
64
64
|
|
|
65
65
|
export type StateHeadMetaRow = typeof stateHeadMetaTable.Type
|
|
66
66
|
|
|
67
|
+
export const REBUILD_META_TABLE = '__livestore_rebuild'
|
|
68
|
+
|
|
69
|
+
/** Only a completed replay and successful migration hooks may insert the marker row. */
|
|
70
|
+
export const rebuildMetaTable = table({
|
|
71
|
+
name: REBUILD_META_TABLE,
|
|
72
|
+
columns: {
|
|
73
|
+
id: SqliteDsl.integer({ primaryKey: true }),
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
67
77
|
/**
|
|
68
78
|
* Table which stores SQLite changeset blobs which is used for rolling back
|
|
69
79
|
* read-model state during rebasing.
|
|
@@ -104,6 +114,7 @@ export const stateSystemTables = [
|
|
|
104
114
|
schemaEventDefsMetaTable,
|
|
105
115
|
stateHeadMetaTable,
|
|
106
116
|
sessionChangesetMetaTable,
|
|
117
|
+
rebuildMetaTable,
|
|
107
118
|
] as const
|
|
108
119
|
|
|
109
120
|
export const isStateSystemTable = (tableName: string) => stateSystemTables.some((_) => _.sqliteDef.name === tableName)
|