@tanstack/db-sqlite-persistence-core 0.4.7 → 0.4.9
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/README.md +32 -3
- package/dist/cjs/broadcast-coordinator.cjs +282 -43
- package/dist/cjs/broadcast-coordinator.cjs.map +1 -1
- package/dist/cjs/broadcast-coordinator.d.cts +25 -8
- package/dist/cjs/committed-tx-anchor.cjs +9 -0
- package/dist/cjs/committed-tx-anchor.cjs.map +1 -0
- package/dist/cjs/committed-tx-anchor.d.cts +1 -0
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/persisted.cjs +1599 -313
- package/dist/cjs/persisted.cjs.map +1 -1
- package/dist/cjs/persisted.d.cts +106 -13
- package/dist/cjs/remote-subset-wire.cjs +11 -0
- package/dist/cjs/remote-subset-wire.cjs.map +1 -1
- package/dist/cjs/remote-subset-wire.d.cts +1 -0
- package/dist/cjs/sqlite-core-adapter.cjs +705 -64
- package/dist/cjs/sqlite-core-adapter.cjs.map +1 -1
- package/dist/cjs/sqlite-core-adapter.d.cts +53 -10
- package/dist/esm/broadcast-coordinator.d.ts +25 -8
- package/dist/esm/broadcast-coordinator.js +283 -44
- package/dist/esm/broadcast-coordinator.js.map +1 -1
- package/dist/esm/committed-tx-anchor.d.ts +1 -0
- package/dist/esm/committed-tx-anchor.js +9 -0
- package/dist/esm/committed-tx-anchor.js.map +1 -0
- package/dist/esm/index.js +6 -2
- package/dist/esm/persisted.d.ts +106 -13
- package/dist/esm/persisted.js +1601 -315
- package/dist/esm/persisted.js.map +1 -1
- package/dist/esm/remote-subset-wire.d.ts +1 -0
- package/dist/esm/remote-subset-wire.js +11 -0
- package/dist/esm/remote-subset-wire.js.map +1 -1
- package/dist/esm/sqlite-core-adapter.d.ts +53 -10
- package/dist/esm/sqlite-core-adapter.js +708 -67
- package/dist/esm/sqlite-core-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/broadcast-coordinator.ts +415 -62
- package/src/committed-tx-anchor.ts +13 -0
- package/src/persisted.ts +2283 -370
- package/src/remote-subset-wire.ts +12 -0
- package/src/sqlite-core-adapter.ts +891 -68
|
@@ -2,17 +2,20 @@ import {
|
|
|
2
2
|
IR,
|
|
3
3
|
compareTemporalValues,
|
|
4
4
|
compileSingleRowExpression,
|
|
5
|
+
safeRandomUUID,
|
|
5
6
|
toBooleanPredicate,
|
|
6
7
|
} from '@tanstack/db'
|
|
7
8
|
import {
|
|
8
9
|
InvalidPersistedCollectionConfigError,
|
|
9
10
|
InvalidPersistedStorageKeyEncodingError,
|
|
10
11
|
} from './errors'
|
|
12
|
+
import { isValidCommittedTxAnchor } from './committed-tx-anchor'
|
|
11
13
|
import {
|
|
12
14
|
SQLITE_DRIVER_SHARED_LOGICAL_SCHEDULING_KEY,
|
|
13
15
|
createPersistedTableName,
|
|
14
16
|
decodePersistedStorageKey,
|
|
15
17
|
encodePersistedStorageKey,
|
|
18
|
+
resolvePersistedStorageTarget,
|
|
16
19
|
} from './persisted'
|
|
17
20
|
import {
|
|
18
21
|
PERSISTED_TYPE_TAG,
|
|
@@ -26,13 +29,17 @@ import {
|
|
|
26
29
|
} from './sqlite-value'
|
|
27
30
|
import type { LoadSubsetOptions } from '@tanstack/db'
|
|
28
31
|
import type {
|
|
32
|
+
CommittedTxAnchor,
|
|
29
33
|
HydrationPersistenceAdapter,
|
|
34
|
+
PersistedCacheGenerationClaim,
|
|
30
35
|
PersistedIndexSpec,
|
|
31
36
|
PersistedKeySetEvidence,
|
|
32
37
|
PersistedRowScanOptions,
|
|
33
38
|
PersistedScannedRow,
|
|
39
|
+
PersistedStorageTarget,
|
|
34
40
|
PersistedTx,
|
|
35
41
|
PersistenceAdapter,
|
|
42
|
+
ReconciledCommittedTx,
|
|
36
43
|
ReplayableTxDelta,
|
|
37
44
|
SQLiteDriver,
|
|
38
45
|
} from './persisted'
|
|
@@ -75,6 +82,10 @@ export type SQLiteCoreAdapterOptions = {
|
|
|
75
82
|
appliedTxPruneMaxRows?: number
|
|
76
83
|
appliedTxPruneMaxAgeSeconds?: number
|
|
77
84
|
pullSinceReloadThreshold?: number
|
|
85
|
+
/** Milliseconds without renewal before a sync run loses cache access. */
|
|
86
|
+
cacheGenerationClaimTtlMs?: number
|
|
87
|
+
/** Host clock, injectable for expiry histories. */
|
|
88
|
+
now?: () => number
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export type SQLitePullSinceResult<TKey extends string | number> =
|
|
@@ -245,6 +256,17 @@ function observeSharedLogicalSchedulingSupport(
|
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
const DEFAULT_SCHEMA_VERSION = 1
|
|
259
|
+
const CACHE_GENERATION_ID_PREFIX = `tanstack-db-cache:`
|
|
260
|
+
const LEGACY_CACHE_GENERATION_ID_PREFIX = `\u0000tanstack-db-cache:`
|
|
261
|
+
|
|
262
|
+
function hasCacheGenerationIdPrefix(collectionId: string): boolean {
|
|
263
|
+
return (
|
|
264
|
+
collectionId.startsWith(CACHE_GENERATION_ID_PREFIX) ||
|
|
265
|
+
collectionId.startsWith(LEGACY_CACHE_GENERATION_ID_PREFIX)
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS = 5 * 60_000
|
|
248
270
|
const DEFAULT_PULL_SINCE_RELOAD_THRESHOLD = 128
|
|
249
271
|
|
|
250
272
|
/**
|
|
@@ -1619,6 +1641,8 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1619
1641
|
private readonly appliedTxPruneMaxAgeSeconds: number | undefined
|
|
1620
1642
|
private readonly pullSinceReloadThreshold: number
|
|
1621
1643
|
private readonly replacementBatchSize: number
|
|
1644
|
+
private readonly cacheGenerationClaimTtlMs: number
|
|
1645
|
+
private readonly now: () => number
|
|
1622
1646
|
|
|
1623
1647
|
private initialized = false
|
|
1624
1648
|
private readonly collectionTableCache = new Map<
|
|
@@ -1630,6 +1654,290 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1630
1654
|
Promise<CollectionTableMapping>
|
|
1631
1655
|
>()
|
|
1632
1656
|
|
|
1657
|
+
claimCacheGeneration(
|
|
1658
|
+
collectionId: string,
|
|
1659
|
+
): Promise<PersistedCacheGenerationClaim> {
|
|
1660
|
+
return this.runRegular(async () => {
|
|
1661
|
+
await this.ensureInitialized()
|
|
1662
|
+
return this.driver.transaction(async (driver) => {
|
|
1663
|
+
const expiresAtMs = this.now() + this.cacheGenerationClaimTtlMs
|
|
1664
|
+
// The first generation must not reuse the legacy collection ID: an older
|
|
1665
|
+
// adapter can still write that ID without a claim after an upgrade.
|
|
1666
|
+
const initialPhysicalId = `${CACHE_GENERATION_ID_PREFIX}${safeRandomUUID()}`
|
|
1667
|
+
await driver.run(
|
|
1668
|
+
`INSERT INTO cache_generation (logical_id, generation, physical_id, retired)
|
|
1669
|
+
SELECT ?, 0, ?, 0
|
|
1670
|
+
WHERE NOT EXISTS (
|
|
1671
|
+
SELECT 1 FROM cache_generation WHERE logical_id = ?
|
|
1672
|
+
)`,
|
|
1673
|
+
[collectionId, initialPhysicalId, collectionId],
|
|
1674
|
+
)
|
|
1675
|
+
const head = await driver.query<{ physical_id: string }>(
|
|
1676
|
+
`SELECT physical_id FROM cache_generation
|
|
1677
|
+
WHERE logical_id = ? AND retired = 0`,
|
|
1678
|
+
[collectionId],
|
|
1679
|
+
)
|
|
1680
|
+
const storageCollectionId = head[0]?.physical_id
|
|
1681
|
+
if (!storageCollectionId) {
|
|
1682
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1683
|
+
`No active persisted cache generation for collection "${collectionId}"`,
|
|
1684
|
+
)
|
|
1685
|
+
}
|
|
1686
|
+
const claimId = safeRandomUUID()
|
|
1687
|
+
await driver.run(
|
|
1688
|
+
`INSERT INTO cache_generation_claim
|
|
1689
|
+
(claim_id, logical_id, physical_id, expires_at_ms)
|
|
1690
|
+
VALUES (?, ?, ?, ?)`,
|
|
1691
|
+
[claimId, collectionId, storageCollectionId, expiresAtMs],
|
|
1692
|
+
)
|
|
1693
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1694
|
+
return { storageCollectionId, claimId, expiresAtMs }
|
|
1695
|
+
})
|
|
1696
|
+
})
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
rotateCacheGeneration(
|
|
1700
|
+
collectionId: string,
|
|
1701
|
+
claimId: string,
|
|
1702
|
+
resetMetadata?: { key: string; value: unknown },
|
|
1703
|
+
expectedStorageCollectionId?: string,
|
|
1704
|
+
): Promise<PersistedCacheGenerationClaim> {
|
|
1705
|
+
return this.runRegular(async () => {
|
|
1706
|
+
await this.ensureInitialized()
|
|
1707
|
+
return this.driver.transaction(async (driver) => {
|
|
1708
|
+
const now = this.now()
|
|
1709
|
+
const expiresAtMs = now + this.cacheGenerationClaimTtlMs
|
|
1710
|
+
const claimed = await driver.query<{
|
|
1711
|
+
physical_id: string
|
|
1712
|
+
expires_at_ms: number
|
|
1713
|
+
}>(
|
|
1714
|
+
`SELECT physical_id, expires_at_ms
|
|
1715
|
+
FROM cache_generation_claim
|
|
1716
|
+
WHERE claim_id = ? AND logical_id = ?`,
|
|
1717
|
+
[claimId, collectionId],
|
|
1718
|
+
)
|
|
1719
|
+
const claimedPhysicalId =
|
|
1720
|
+
claimed[0] && claimed[0].expires_at_ms > now
|
|
1721
|
+
? claimed[0].physical_id
|
|
1722
|
+
: undefined
|
|
1723
|
+
if (
|
|
1724
|
+
claimedPhysicalId &&
|
|
1725
|
+
expectedStorageCollectionId &&
|
|
1726
|
+
claimedPhysicalId !== expectedStorageCollectionId
|
|
1727
|
+
) {
|
|
1728
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1729
|
+
`Persisted cache claim changed generations before recovery`,
|
|
1730
|
+
)
|
|
1731
|
+
}
|
|
1732
|
+
const head = await driver.query<{
|
|
1733
|
+
generation: number
|
|
1734
|
+
physical_id: string
|
|
1735
|
+
}>(
|
|
1736
|
+
`SELECT generation, physical_id FROM cache_generation
|
|
1737
|
+
WHERE logical_id = ? AND retired = 0`,
|
|
1738
|
+
[collectionId],
|
|
1739
|
+
)
|
|
1740
|
+
const active = head[0]
|
|
1741
|
+
if (!active) {
|
|
1742
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1743
|
+
`No active persisted cache generation for collection "${collectionId}"`,
|
|
1744
|
+
)
|
|
1745
|
+
}
|
|
1746
|
+
await driver.run(
|
|
1747
|
+
`INSERT INTO cache_generation_sequence (logical_id, next_generation)
|
|
1748
|
+
SELECT ?, COALESCE(MAX(generation), -1) + 1
|
|
1749
|
+
FROM cache_generation WHERE logical_id = ?
|
|
1750
|
+
ON CONFLICT(logical_id) DO NOTHING`,
|
|
1751
|
+
[collectionId, collectionId],
|
|
1752
|
+
)
|
|
1753
|
+
const nextRows = await driver.query<{ next_generation: number }>(
|
|
1754
|
+
`SELECT next_generation FROM cache_generation_sequence
|
|
1755
|
+
WHERE logical_id = ?`,
|
|
1756
|
+
[collectionId],
|
|
1757
|
+
)
|
|
1758
|
+
const nextGeneration = nextRows[0]!.next_generation
|
|
1759
|
+
await driver.run(
|
|
1760
|
+
`UPDATE cache_generation_sequence SET next_generation = ?
|
|
1761
|
+
WHERE logical_id = ?`,
|
|
1762
|
+
[nextGeneration + 1, collectionId],
|
|
1763
|
+
)
|
|
1764
|
+
// An expired claim no longer owns the head, even when it remembers
|
|
1765
|
+
// the current storage ID. Its recovery is private so another run's
|
|
1766
|
+
// valid current cache remains claimable.
|
|
1767
|
+
const ownedHead = claimedPhysicalId === active.physical_id
|
|
1768
|
+
const storageCollectionId = `${CACHE_GENERATION_ID_PREFIX}${safeRandomUUID()}`
|
|
1769
|
+
if (ownedHead) {
|
|
1770
|
+
await driver.run(
|
|
1771
|
+
`UPDATE cache_generation SET retired = 1
|
|
1772
|
+
WHERE logical_id = ? AND generation = ?`,
|
|
1773
|
+
[collectionId, active.generation],
|
|
1774
|
+
)
|
|
1775
|
+
}
|
|
1776
|
+
await driver.run(
|
|
1777
|
+
`INSERT INTO cache_generation (logical_id, generation, physical_id, retired)
|
|
1778
|
+
VALUES (?, ?, ?, ?)`,
|
|
1779
|
+
[
|
|
1780
|
+
collectionId,
|
|
1781
|
+
nextGeneration,
|
|
1782
|
+
storageCollectionId,
|
|
1783
|
+
ownedHead ? 0 : 1,
|
|
1784
|
+
],
|
|
1785
|
+
)
|
|
1786
|
+
const nextClaimId = claimed[0] ? claimId : safeRandomUUID()
|
|
1787
|
+
if (claimed[0]) {
|
|
1788
|
+
await driver.run(
|
|
1789
|
+
`UPDATE cache_generation_claim
|
|
1790
|
+
SET physical_id = ?, expires_at_ms = ? WHERE claim_id = ?`,
|
|
1791
|
+
[storageCollectionId, expiresAtMs, claimId],
|
|
1792
|
+
)
|
|
1793
|
+
} else {
|
|
1794
|
+
await driver.run(
|
|
1795
|
+
`INSERT INTO cache_generation_claim
|
|
1796
|
+
(claim_id, logical_id, physical_id, expires_at_ms)
|
|
1797
|
+
VALUES (?, ?, ?, ?)`,
|
|
1798
|
+
[nextClaimId, collectionId, storageCollectionId, expiresAtMs],
|
|
1799
|
+
)
|
|
1800
|
+
}
|
|
1801
|
+
await driver.run(
|
|
1802
|
+
`INSERT INTO collection_version (
|
|
1803
|
+
collection_id, latest_row_version,
|
|
1804
|
+
key_set_evidence_available, key_set_evidence_incompatible
|
|
1805
|
+
) VALUES (?, 0, 1, 1)`,
|
|
1806
|
+
[storageCollectionId],
|
|
1807
|
+
)
|
|
1808
|
+
if (resetMetadata) {
|
|
1809
|
+
await driver.run(
|
|
1810
|
+
`INSERT INTO collection_metadata (collection_id, key, value, updated_at)
|
|
1811
|
+
VALUES (?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))`,
|
|
1812
|
+
[
|
|
1813
|
+
storageCollectionId,
|
|
1814
|
+
resetMetadata.key,
|
|
1815
|
+
serializePersistedRowValue(resetMetadata.value),
|
|
1816
|
+
],
|
|
1817
|
+
)
|
|
1818
|
+
}
|
|
1819
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1820
|
+
return { storageCollectionId, claimId: nextClaimId, expiresAtMs }
|
|
1821
|
+
})
|
|
1822
|
+
})
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
releaseCacheGenerationClaim(claimId: string): Promise<void> {
|
|
1826
|
+
return this.runRegular(async () => {
|
|
1827
|
+
await this.ensureInitialized()
|
|
1828
|
+
await this.runInTransaction(async (driver) => {
|
|
1829
|
+
await driver.run(
|
|
1830
|
+
`DELETE FROM cache_generation_claim WHERE claim_id = ?`,
|
|
1831
|
+
[claimId],
|
|
1832
|
+
)
|
|
1833
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1834
|
+
})
|
|
1835
|
+
})
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
renewCacheGenerationClaim(
|
|
1839
|
+
storageCollectionId: string,
|
|
1840
|
+
claimId: string,
|
|
1841
|
+
): Promise<number | undefined> {
|
|
1842
|
+
return this.runRegular(() =>
|
|
1843
|
+
this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
|
|
1844
|
+
)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
private async renewCacheGenerationClaimUnscheduled(
|
|
1848
|
+
storageCollectionId: string,
|
|
1849
|
+
claimId: string,
|
|
1850
|
+
): Promise<number | undefined> {
|
|
1851
|
+
await this.ensureInitialized()
|
|
1852
|
+
return this.runInTransaction(async (driver) => {
|
|
1853
|
+
const now = this.now()
|
|
1854
|
+
const rows = await driver.query<{ claim_id: string }>(
|
|
1855
|
+
`SELECT claim_id FROM cache_generation_claim
|
|
1856
|
+
WHERE claim_id = ? AND physical_id = ? AND expires_at_ms > ?`,
|
|
1857
|
+
[claimId, storageCollectionId, now],
|
|
1858
|
+
)
|
|
1859
|
+
if (!rows[0]) {
|
|
1860
|
+
await this.collectRetiredCacheGenerations(driver)
|
|
1861
|
+
return undefined
|
|
1862
|
+
}
|
|
1863
|
+
const expiresAtMs = now + this.cacheGenerationClaimTtlMs
|
|
1864
|
+
await driver.run(
|
|
1865
|
+
`UPDATE cache_generation_claim SET expires_at_ms = ?
|
|
1866
|
+
WHERE claim_id = ?`,
|
|
1867
|
+
[expiresAtMs, claimId],
|
|
1868
|
+
)
|
|
1869
|
+
return expiresAtMs
|
|
1870
|
+
})
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
private async collectRetiredCacheGenerations(
|
|
1874
|
+
driver: SQLiteDriver,
|
|
1875
|
+
): Promise<void> {
|
|
1876
|
+
await driver.run(
|
|
1877
|
+
`DELETE FROM cache_generation_claim WHERE expires_at_ms <= ?`,
|
|
1878
|
+
[this.now()],
|
|
1879
|
+
)
|
|
1880
|
+
const retired = await driver.query<{
|
|
1881
|
+
physical_id: string
|
|
1882
|
+
table_name: string | null
|
|
1883
|
+
tombstone_table_name: string | null
|
|
1884
|
+
}>(
|
|
1885
|
+
`SELECT generation.physical_id, registry.table_name,
|
|
1886
|
+
registry.tombstone_table_name
|
|
1887
|
+
FROM cache_generation AS generation
|
|
1888
|
+
LEFT JOIN collection_registry AS registry
|
|
1889
|
+
ON registry.collection_id = generation.physical_id
|
|
1890
|
+
WHERE generation.retired = 1
|
|
1891
|
+
AND NOT EXISTS (
|
|
1892
|
+
SELECT 1 FROM cache_generation_claim AS claim
|
|
1893
|
+
WHERE claim.physical_id = generation.physical_id
|
|
1894
|
+
)`,
|
|
1895
|
+
)
|
|
1896
|
+
for (const generation of retired) {
|
|
1897
|
+
if (generation.table_name) {
|
|
1898
|
+
await driver.exec(
|
|
1899
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(generation.table_name)}`,
|
|
1900
|
+
)
|
|
1901
|
+
}
|
|
1902
|
+
if (generation.tombstone_table_name) {
|
|
1903
|
+
await driver.exec(
|
|
1904
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(generation.tombstone_table_name)}`,
|
|
1905
|
+
)
|
|
1906
|
+
}
|
|
1907
|
+
for (const table of [
|
|
1908
|
+
`applied_tx`,
|
|
1909
|
+
`collection_expected_keys`,
|
|
1910
|
+
`collection_metadata`,
|
|
1911
|
+
`persisted_index_registry`,
|
|
1912
|
+
`leader_term`,
|
|
1913
|
+
]) {
|
|
1914
|
+
await driver.run(`DELETE FROM ${table} WHERE collection_id = ?`, [
|
|
1915
|
+
generation.physical_id,
|
|
1916
|
+
])
|
|
1917
|
+
}
|
|
1918
|
+
await driver.run(
|
|
1919
|
+
`DELETE FROM collection_version WHERE collection_id = ?`,
|
|
1920
|
+
[generation.physical_id],
|
|
1921
|
+
)
|
|
1922
|
+
await driver.run(
|
|
1923
|
+
`DELETE FROM collection_reset_epoch WHERE collection_id = ?`,
|
|
1924
|
+
[generation.physical_id],
|
|
1925
|
+
)
|
|
1926
|
+
await driver.run(
|
|
1927
|
+
`DELETE FROM collection_registry WHERE collection_id = ?`,
|
|
1928
|
+
[generation.physical_id],
|
|
1929
|
+
)
|
|
1930
|
+
await driver.run(`DELETE FROM cache_generation WHERE physical_id = ?`, [
|
|
1931
|
+
generation.physical_id,
|
|
1932
|
+
])
|
|
1933
|
+
this.collectionTableCache.delete(generation.physical_id)
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
getCacheGenerationNow(): number {
|
|
1938
|
+
return this.now()
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1633
1941
|
constructor(options: SQLiteCoreAdapterOptions) {
|
|
1634
1942
|
const maxBoundParameters = options.driver.maxBoundParameters
|
|
1635
1943
|
if (
|
|
@@ -1645,6 +1953,18 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1645
1953
|
? REPLACEMENT_BATCH_SIZE
|
|
1646
1954
|
: Math.min(REPLACEMENT_BATCH_SIZE, Math.floor(maxBoundParameters / 4))
|
|
1647
1955
|
|
|
1956
|
+
this.cacheGenerationClaimTtlMs =
|
|
1957
|
+
options.cacheGenerationClaimTtlMs ?? DEFAULT_CACHE_GENERATION_CLAIM_TTL_MS
|
|
1958
|
+
if (
|
|
1959
|
+
!Number.isSafeInteger(this.cacheGenerationClaimTtlMs) ||
|
|
1960
|
+
this.cacheGenerationClaimTtlMs <= 0
|
|
1961
|
+
) {
|
|
1962
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
1963
|
+
`SQLite adapter cacheGenerationClaimTtlMs must be a positive safe integer`,
|
|
1964
|
+
)
|
|
1965
|
+
}
|
|
1966
|
+
this.now = options.now ?? Date.now
|
|
1967
|
+
|
|
1648
1968
|
const schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION
|
|
1649
1969
|
if (!Number.isInteger(schemaVersion) || schemaVersion < 0) {
|
|
1650
1970
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -1713,18 +2033,22 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1713
2033
|
this.loadResumeSnapshotUnscheduled(collectionId, context),
|
|
1714
2034
|
applyCommittedTx: (collectionId, tx) =>
|
|
1715
2035
|
this.applyCommittedTxUnscheduled(collectionId, tx),
|
|
1716
|
-
|
|
1717
|
-
this.
|
|
1718
|
-
|
|
1719
|
-
this.
|
|
1720
|
-
|
|
1721
|
-
this.
|
|
1722
|
-
|
|
1723
|
-
this.
|
|
1724
|
-
|
|
1725
|
-
this.
|
|
1726
|
-
|
|
1727
|
-
this.
|
|
2036
|
+
renewCacheGenerationClaim: (storageCollectionId, claimId) =>
|
|
2037
|
+
this.renewCacheGenerationClaimUnscheduled(storageCollectionId, claimId),
|
|
2038
|
+
reconcileCommittedTx: (collectionId, tx, anchor) =>
|
|
2039
|
+
this.reconcileCommittedTxUnscheduled(collectionId, tx, anchor),
|
|
2040
|
+
loadCollectionMetadata: (collectionId, context) =>
|
|
2041
|
+
this.loadCollectionMetadataUnscheduled(collectionId, context),
|
|
2042
|
+
scanRows: (collectionId, scanOptions, context) =>
|
|
2043
|
+
this.scanRowsUnscheduled(collectionId, scanOptions, context),
|
|
2044
|
+
ensureIndex: (collectionId, signature, spec, ctx) =>
|
|
2045
|
+
this.ensureIndexUnscheduled(collectionId, signature, spec, ctx),
|
|
2046
|
+
markIndexRemoved: (collectionId, signature, ctx) =>
|
|
2047
|
+
this.markIndexRemovedUnscheduled(collectionId, signature, ctx),
|
|
2048
|
+
getStreamPosition: (collectionId, ctx) =>
|
|
2049
|
+
this.getStreamPositionUnscheduled(collectionId, ctx),
|
|
2050
|
+
pullSince: (collectionId, fromRowVersion, ctx) =>
|
|
2051
|
+
this.pullSinceUnscheduled(collectionId, fromRowVersion, ctx),
|
|
1728
2052
|
runInHydrationScope: async (task) => task(this.hydrationAdapter),
|
|
1729
2053
|
}
|
|
1730
2054
|
}
|
|
@@ -1804,10 +2128,48 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1804
2128
|
}
|
|
1805
2129
|
}
|
|
1806
2130
|
|
|
2131
|
+
private async assertCacheGenerationReadClaim(
|
|
2132
|
+
driver: SQLiteDriver,
|
|
2133
|
+
storageCollectionId: string,
|
|
2134
|
+
claimId?: string,
|
|
2135
|
+
): Promise<void> {
|
|
2136
|
+
await this.ensureInitialized()
|
|
2137
|
+
if (!claimId) {
|
|
2138
|
+
if (!hasCacheGenerationIdPrefix(storageCollectionId)) return
|
|
2139
|
+
const generation = await driver.query<{ physical_id: string }>(
|
|
2140
|
+
`SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
|
|
2141
|
+
[storageCollectionId],
|
|
2142
|
+
)
|
|
2143
|
+
if (generation[0]) {
|
|
2144
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2145
|
+
`Persisted cache claim is required for collection "${storageCollectionId}"`,
|
|
2146
|
+
)
|
|
2147
|
+
}
|
|
2148
|
+
return
|
|
2149
|
+
}
|
|
2150
|
+
const rows = await driver.query<{ claim_id: string }>(
|
|
2151
|
+
`SELECT claim.claim_id
|
|
2152
|
+
FROM cache_generation_claim AS claim
|
|
2153
|
+
JOIN cache_generation AS generation
|
|
2154
|
+
ON generation.physical_id = claim.physical_id
|
|
2155
|
+
WHERE claim.claim_id = ? AND claim.physical_id = ?
|
|
2156
|
+
AND claim.expires_at_ms > ?`,
|
|
2157
|
+
[claimId, storageCollectionId, this.now()],
|
|
2158
|
+
)
|
|
2159
|
+
if (!rows[0]) {
|
|
2160
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
2161
|
+
`Persisted cache claim is no longer active for collection "${storageCollectionId}"`,
|
|
2162
|
+
)
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
1807
2166
|
loadSubset(
|
|
1808
|
-
|
|
2167
|
+
target: PersistedStorageTarget,
|
|
1809
2168
|
options: LoadSubsetOptions,
|
|
1810
|
-
ctx?: {
|
|
2169
|
+
ctx?: {
|
|
2170
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
2171
|
+
cacheGenerationClaimId?: string
|
|
2172
|
+
},
|
|
1811
2173
|
): Promise<
|
|
1812
2174
|
Array<{
|
|
1813
2175
|
key: string | number
|
|
@@ -1816,14 +2178,17 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1816
2178
|
}>
|
|
1817
2179
|
> {
|
|
1818
2180
|
return this.runRegular(() =>
|
|
1819
|
-
this.loadSubsetUnscheduled(
|
|
2181
|
+
this.loadSubsetUnscheduled(target, options, ctx),
|
|
1820
2182
|
)
|
|
1821
2183
|
}
|
|
1822
2184
|
|
|
1823
2185
|
private async loadSubsetUnscheduled(
|
|
1824
|
-
|
|
2186
|
+
target: PersistedStorageTarget,
|
|
1825
2187
|
options: LoadSubsetOptions,
|
|
1826
|
-
ctx?: {
|
|
2188
|
+
ctx?: {
|
|
2189
|
+
requiredIndexSignatures?: ReadonlyArray<string>
|
|
2190
|
+
cacheGenerationClaimId?: string
|
|
2191
|
+
},
|
|
1827
2192
|
): Promise<
|
|
1828
2193
|
Array<{
|
|
1829
2194
|
key: string | number
|
|
@@ -1831,8 +2196,29 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1831
2196
|
metadata?: unknown
|
|
1832
2197
|
}>
|
|
1833
2198
|
> {
|
|
1834
|
-
const
|
|
2199
|
+
const access = resolvePersistedStorageTarget(
|
|
2200
|
+
target,
|
|
2201
|
+
ctx?.cacheGenerationClaimId,
|
|
2202
|
+
)
|
|
2203
|
+
const collectionId = access.collectionId
|
|
2204
|
+
ctx = access.claimId
|
|
2205
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2206
|
+
: ctx
|
|
2207
|
+
await this.assertCacheGenerationReadClaim(
|
|
2208
|
+
this.driver,
|
|
2209
|
+
collectionId,
|
|
2210
|
+
ctx?.cacheGenerationClaimId,
|
|
2211
|
+
)
|
|
2212
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2213
|
+
collectionId,
|
|
2214
|
+
ctx?.cacheGenerationClaimId,
|
|
2215
|
+
)
|
|
1835
2216
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2217
|
+
await this.assertCacheGenerationReadClaim(
|
|
2218
|
+
transactionDriver,
|
|
2219
|
+
collectionId,
|
|
2220
|
+
ctx?.cacheGenerationClaimId,
|
|
2221
|
+
)
|
|
1836
2222
|
await this.assertCurrentSchemaVersion(
|
|
1837
2223
|
collectionId,
|
|
1838
2224
|
transactionDriver,
|
|
@@ -1906,22 +2292,24 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1906
2292
|
}
|
|
1907
2293
|
|
|
1908
2294
|
loadResumeSnapshot(
|
|
1909
|
-
|
|
2295
|
+
target: PersistedStorageTarget,
|
|
1910
2296
|
ctx?: {
|
|
1911
2297
|
requiredIndexSignatures?: ReadonlyArray<string>
|
|
1912
2298
|
includeRows?: boolean
|
|
2299
|
+
cacheGenerationClaimId?: string
|
|
1913
2300
|
},
|
|
1914
2301
|
) {
|
|
1915
2302
|
return this.runRegular(() =>
|
|
1916
|
-
this.loadResumeSnapshotUnscheduled(
|
|
2303
|
+
this.loadResumeSnapshotUnscheduled(target, ctx),
|
|
1917
2304
|
)
|
|
1918
2305
|
}
|
|
1919
2306
|
|
|
1920
2307
|
private async loadResumeSnapshotUnscheduled(
|
|
1921
|
-
|
|
2308
|
+
target: PersistedStorageTarget,
|
|
1922
2309
|
ctx?: {
|
|
1923
2310
|
requiredIndexSignatures?: ReadonlyArray<string>
|
|
1924
2311
|
includeRows?: boolean
|
|
2312
|
+
cacheGenerationClaimId?: string
|
|
1925
2313
|
},
|
|
1926
2314
|
): Promise<{
|
|
1927
2315
|
rows: Array<{
|
|
@@ -1936,10 +2324,31 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
1936
2324
|
latestRowVersion: number
|
|
1937
2325
|
resetEpoch: number
|
|
1938
2326
|
}> {
|
|
1939
|
-
const
|
|
2327
|
+
const access = resolvePersistedStorageTarget(
|
|
2328
|
+
target,
|
|
2329
|
+
ctx?.cacheGenerationClaimId,
|
|
2330
|
+
)
|
|
2331
|
+
const collectionId = access.collectionId
|
|
2332
|
+
ctx = access.claimId
|
|
2333
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2334
|
+
: ctx
|
|
2335
|
+
await this.assertCacheGenerationReadClaim(
|
|
2336
|
+
this.driver,
|
|
2337
|
+
collectionId,
|
|
2338
|
+
ctx?.cacheGenerationClaimId,
|
|
2339
|
+
)
|
|
2340
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2341
|
+
collectionId,
|
|
2342
|
+
ctx?.cacheGenerationClaimId,
|
|
2343
|
+
)
|
|
1940
2344
|
const includeRows = ctx?.includeRows !== false
|
|
1941
2345
|
|
|
1942
2346
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2347
|
+
await this.assertCacheGenerationReadClaim(
|
|
2348
|
+
transactionDriver,
|
|
2349
|
+
collectionId,
|
|
2350
|
+
ctx?.cacheGenerationClaimId,
|
|
2351
|
+
)
|
|
1943
2352
|
await this.assertCurrentSchemaVersion(
|
|
1944
2353
|
collectionId,
|
|
1945
2354
|
transactionDriver,
|
|
@@ -2000,30 +2409,84 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2000
2409
|
})
|
|
2001
2410
|
}
|
|
2002
2411
|
|
|
2003
|
-
applyCommittedTx(
|
|
2412
|
+
applyCommittedTx(
|
|
2413
|
+
target: PersistedStorageTarget,
|
|
2414
|
+
tx: PersistedTx,
|
|
2415
|
+
): Promise<void> {
|
|
2416
|
+
return this.runRegular(() => this.applyCommittedTxUnscheduled(target, tx))
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
reconcileCommittedTx(
|
|
2420
|
+
target: PersistedStorageTarget,
|
|
2421
|
+
tx: PersistedTx,
|
|
2422
|
+
anchor: CommittedTxAnchor,
|
|
2423
|
+
): Promise<ReconciledCommittedTx> {
|
|
2004
2424
|
return this.runRegular(() =>
|
|
2005
|
-
this.
|
|
2425
|
+
this.reconcileCommittedTxUnscheduled(target, tx, anchor),
|
|
2006
2426
|
)
|
|
2007
2427
|
}
|
|
2008
2428
|
|
|
2009
2429
|
private async applyCommittedTxUnscheduled(
|
|
2010
|
-
|
|
2430
|
+
target: PersistedStorageTarget,
|
|
2011
2431
|
tx: PersistedTx,
|
|
2012
2432
|
): Promise<void> {
|
|
2013
|
-
|
|
2433
|
+
await this.applyCommittedTxInternal(target, tx)
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
private reconcileCommittedTxUnscheduled(
|
|
2437
|
+
target: PersistedStorageTarget,
|
|
2438
|
+
tx: PersistedTx,
|
|
2439
|
+
anchor: CommittedTxAnchor,
|
|
2440
|
+
): Promise<ReconciledCommittedTx> {
|
|
2441
|
+
if (!isValidCommittedTxAnchor(anchor)) {
|
|
2442
|
+
throw new Error(
|
|
2443
|
+
`Cannot reconcile a committed transaction without a valid durable anchor`,
|
|
2444
|
+
)
|
|
2445
|
+
}
|
|
2446
|
+
return this.applyCommittedTxInternal(target, tx, anchor)
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
private async applyCommittedTxInternal(
|
|
2450
|
+
target: PersistedStorageTarget,
|
|
2451
|
+
tx: PersistedTx,
|
|
2452
|
+
anchor?: CommittedTxAnchor,
|
|
2453
|
+
): Promise<ReconciledCommittedTx> {
|
|
2454
|
+
const access = resolvePersistedStorageTarget(
|
|
2455
|
+
target,
|
|
2456
|
+
tx.cacheGenerationClaimId,
|
|
2457
|
+
)
|
|
2458
|
+
const collectionId = access.collectionId
|
|
2459
|
+
tx = { ...tx, cacheGenerationClaimId: access.claimId }
|
|
2460
|
+
await this.assertCacheGenerationReadClaim(
|
|
2461
|
+
this.driver,
|
|
2462
|
+
collectionId,
|
|
2463
|
+
tx.cacheGenerationClaimId,
|
|
2464
|
+
)
|
|
2465
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2466
|
+
collectionId,
|
|
2467
|
+
tx.cacheGenerationClaimId,
|
|
2468
|
+
)
|
|
2014
2469
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2015
2470
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
2016
2471
|
|
|
2017
|
-
|
|
2472
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
2473
|
+
await this.assertCacheGenerationReadClaim(
|
|
2474
|
+
transactionDriver,
|
|
2475
|
+
collectionId,
|
|
2476
|
+
tx.cacheGenerationClaimId,
|
|
2477
|
+
)
|
|
2018
2478
|
const versionRows = await transactionDriver.query<{
|
|
2019
2479
|
latest_row_version: number
|
|
2020
2480
|
key_set_evidence_available: number
|
|
2021
2481
|
schema_version: number
|
|
2022
2482
|
already_applied: number
|
|
2483
|
+
reset_epoch: number | null
|
|
2023
2484
|
}>(
|
|
2024
2485
|
`SELECT
|
|
2025
2486
|
latest_row_version,
|
|
2026
2487
|
key_set_evidence_available,
|
|
2488
|
+
(SELECT reset_epoch FROM collection_reset_epoch
|
|
2489
|
+
WHERE collection_id = ? LIMIT 1) AS reset_epoch,
|
|
2027
2490
|
(
|
|
2028
2491
|
SELECT schema_version
|
|
2029
2492
|
FROM collection_registry
|
|
@@ -2038,7 +2501,14 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2038
2501
|
FROM collection_version
|
|
2039
2502
|
WHERE collection_id = ?
|
|
2040
2503
|
LIMIT 1`,
|
|
2041
|
-
[
|
|
2504
|
+
[
|
|
2505
|
+
collectionId,
|
|
2506
|
+
collectionId,
|
|
2507
|
+
collectionId,
|
|
2508
|
+
tx.term,
|
|
2509
|
+
tx.seq,
|
|
2510
|
+
collectionId,
|
|
2511
|
+
],
|
|
2042
2512
|
)
|
|
2043
2513
|
const version = versionRows[0]
|
|
2044
2514
|
|
|
@@ -2055,8 +2525,47 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2055
2525
|
)
|
|
2056
2526
|
}
|
|
2057
2527
|
|
|
2528
|
+
if (anchor) {
|
|
2529
|
+
const sameResetEpoch = (version.reset_epoch ?? 0) === anchor.resetEpoch
|
|
2530
|
+
const matching = await transactionDriver.query<{
|
|
2531
|
+
term: number
|
|
2532
|
+
seq: number
|
|
2533
|
+
row_version: number
|
|
2534
|
+
}>(
|
|
2535
|
+
`SELECT term, seq, row_version FROM applied_tx
|
|
2536
|
+
WHERE collection_id = ? AND tx_id = ? LIMIT 2`,
|
|
2537
|
+
[collectionId, tx.txId],
|
|
2538
|
+
)
|
|
2539
|
+
if (matching.length > 1) return { kind: `unknown` }
|
|
2540
|
+
if (matching[0]) {
|
|
2541
|
+
if (
|
|
2542
|
+
matching[0].row_version <= anchor.latestRowVersion ||
|
|
2543
|
+
!sameResetEpoch
|
|
2544
|
+
) {
|
|
2545
|
+
return { kind: `unknown` }
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
kind: `already-applied`,
|
|
2549
|
+
committed: {
|
|
2550
|
+
term: matching[0].term,
|
|
2551
|
+
seq: matching[0].seq,
|
|
2552
|
+
rowVersion: matching[0].row_version,
|
|
2553
|
+
},
|
|
2554
|
+
latestRowVersion: version.latest_row_version,
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
// A different durable write may have superseded this source row or
|
|
2558
|
+
// its opaque cursor. Absence alone cannot authorize late application.
|
|
2559
|
+
if (
|
|
2560
|
+
version.latest_row_version !== anchor.latestRowVersion ||
|
|
2561
|
+
!sameResetEpoch
|
|
2562
|
+
) {
|
|
2563
|
+
return { kind: `unknown` }
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2058
2567
|
if (version.already_applied === 1) {
|
|
2059
|
-
return
|
|
2568
|
+
return { kind: `unknown` }
|
|
2060
2569
|
}
|
|
2061
2570
|
|
|
2062
2571
|
const currentRowVersion = version.latest_row_version
|
|
@@ -2366,22 +2875,51 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2366
2875
|
)
|
|
2367
2876
|
|
|
2368
2877
|
await this.pruneAppliedTxRows(collectionId, transactionDriver)
|
|
2878
|
+
return {
|
|
2879
|
+
kind: `applied-now`,
|
|
2880
|
+
committed: {
|
|
2881
|
+
term: tx.term,
|
|
2882
|
+
seq: tx.seq,
|
|
2883
|
+
rowVersion: nextRowVersion,
|
|
2884
|
+
},
|
|
2885
|
+
latestRowVersion: nextRowVersion,
|
|
2886
|
+
}
|
|
2369
2887
|
})
|
|
2370
2888
|
}
|
|
2371
2889
|
|
|
2372
2890
|
loadCollectionMetadata(
|
|
2373
|
-
|
|
2891
|
+
target: PersistedStorageTarget,
|
|
2892
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2374
2893
|
): Promise<Array<{ key: string; value: unknown }>> {
|
|
2375
2894
|
return this.runRegular(() =>
|
|
2376
|
-
this.loadCollectionMetadataUnscheduled(
|
|
2895
|
+
this.loadCollectionMetadataUnscheduled(target, ctx),
|
|
2377
2896
|
)
|
|
2378
2897
|
}
|
|
2379
2898
|
|
|
2380
2899
|
private async loadCollectionMetadataUnscheduled(
|
|
2381
|
-
|
|
2900
|
+
target: PersistedStorageTarget,
|
|
2901
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2382
2902
|
): Promise<Array<{ key: string; value: unknown }>> {
|
|
2383
|
-
|
|
2903
|
+
const access = resolvePersistedStorageTarget(
|
|
2904
|
+
target,
|
|
2905
|
+
ctx?.cacheGenerationClaimId,
|
|
2906
|
+
)
|
|
2907
|
+
const collectionId = access.collectionId
|
|
2908
|
+
ctx = access.claimId
|
|
2909
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2910
|
+
: ctx
|
|
2911
|
+
await this.assertCacheGenerationReadClaim(
|
|
2912
|
+
this.driver,
|
|
2913
|
+
collectionId,
|
|
2914
|
+
ctx?.cacheGenerationClaimId,
|
|
2915
|
+
)
|
|
2916
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2384
2917
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2918
|
+
await this.assertCacheGenerationReadClaim(
|
|
2919
|
+
transactionDriver,
|
|
2920
|
+
collectionId,
|
|
2921
|
+
ctx?.cacheGenerationClaimId,
|
|
2922
|
+
)
|
|
2385
2923
|
await this.assertCurrentSchemaVersion(
|
|
2386
2924
|
collectionId,
|
|
2387
2925
|
transactionDriver,
|
|
@@ -2405,21 +2943,42 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2405
2943
|
}
|
|
2406
2944
|
|
|
2407
2945
|
scanRows(
|
|
2408
|
-
|
|
2946
|
+
target: PersistedStorageTarget,
|
|
2409
2947
|
options?: PersistedRowScanOptions,
|
|
2948
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2410
2949
|
): Promise<Array<PersistedScannedRow>> {
|
|
2411
|
-
return this.runRegular(() =>
|
|
2412
|
-
this.scanRowsUnscheduled(collectionId, options),
|
|
2413
|
-
)
|
|
2950
|
+
return this.runRegular(() => this.scanRowsUnscheduled(target, options, ctx))
|
|
2414
2951
|
}
|
|
2415
2952
|
|
|
2416
2953
|
private async scanRowsUnscheduled(
|
|
2417
|
-
|
|
2954
|
+
target: PersistedStorageTarget,
|
|
2418
2955
|
options?: PersistedRowScanOptions,
|
|
2956
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2419
2957
|
): Promise<Array<PersistedScannedRow>> {
|
|
2420
|
-
const
|
|
2958
|
+
const access = resolvePersistedStorageTarget(
|
|
2959
|
+
target,
|
|
2960
|
+
ctx?.cacheGenerationClaimId,
|
|
2961
|
+
)
|
|
2962
|
+
const collectionId = access.collectionId
|
|
2963
|
+
ctx = access.claimId
|
|
2964
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
2965
|
+
: ctx
|
|
2966
|
+
await this.assertCacheGenerationReadClaim(
|
|
2967
|
+
this.driver,
|
|
2968
|
+
collectionId,
|
|
2969
|
+
ctx?.cacheGenerationClaimId,
|
|
2970
|
+
)
|
|
2971
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
2972
|
+
collectionId,
|
|
2973
|
+
ctx?.cacheGenerationClaimId,
|
|
2974
|
+
)
|
|
2421
2975
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2422
2976
|
return this.runInTransaction(async (transactionDriver) => {
|
|
2977
|
+
await this.assertCacheGenerationReadClaim(
|
|
2978
|
+
transactionDriver,
|
|
2979
|
+
collectionId,
|
|
2980
|
+
ctx?.cacheGenerationClaimId,
|
|
2981
|
+
)
|
|
2423
2982
|
await this.assertCurrentSchemaVersion(
|
|
2424
2983
|
collectionId,
|
|
2425
2984
|
transactionDriver,
|
|
@@ -2443,21 +3002,39 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2443
3002
|
}
|
|
2444
3003
|
|
|
2445
3004
|
ensureIndex(
|
|
2446
|
-
|
|
3005
|
+
target: PersistedStorageTarget,
|
|
2447
3006
|
signature: string,
|
|
2448
3007
|
spec: PersistedIndexSpec,
|
|
3008
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2449
3009
|
): Promise<void> {
|
|
2450
3010
|
return this.runRegular(() =>
|
|
2451
|
-
this.ensureIndexUnscheduled(
|
|
3011
|
+
this.ensureIndexUnscheduled(target, signature, spec, ctx),
|
|
2452
3012
|
)
|
|
2453
3013
|
}
|
|
2454
3014
|
|
|
2455
3015
|
private async ensureIndexUnscheduled(
|
|
2456
|
-
|
|
3016
|
+
target: PersistedStorageTarget,
|
|
2457
3017
|
signature: string,
|
|
2458
3018
|
spec: PersistedIndexSpec,
|
|
3019
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2459
3020
|
): Promise<void> {
|
|
2460
|
-
const
|
|
3021
|
+
const access = resolvePersistedStorageTarget(
|
|
3022
|
+
target,
|
|
3023
|
+
ctx?.cacheGenerationClaimId,
|
|
3024
|
+
)
|
|
3025
|
+
const collectionId = access.collectionId
|
|
3026
|
+
ctx = access.claimId
|
|
3027
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3028
|
+
: ctx
|
|
3029
|
+
await this.assertCacheGenerationReadClaim(
|
|
3030
|
+
this.driver,
|
|
3031
|
+
collectionId,
|
|
3032
|
+
ctx?.cacheGenerationClaimId,
|
|
3033
|
+
)
|
|
3034
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
3035
|
+
collectionId,
|
|
3036
|
+
ctx?.cacheGenerationClaimId,
|
|
3037
|
+
)
|
|
2461
3038
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2462
3039
|
const indexName = buildIndexName(collectionId, signature)
|
|
2463
3040
|
const indexNameSql = quoteIdentifier(indexName)
|
|
@@ -2472,6 +3049,11 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2472
3049
|
const persistedWhereSql = whereSql ?? null
|
|
2473
3050
|
|
|
2474
3051
|
await this.runInTransaction(async (transactionDriver) => {
|
|
3052
|
+
await this.assertCacheGenerationReadClaim(
|
|
3053
|
+
transactionDriver,
|
|
3054
|
+
collectionId,
|
|
3055
|
+
ctx?.cacheGenerationClaimId,
|
|
3056
|
+
)
|
|
2475
3057
|
await this.assertCurrentSchemaVersion(
|
|
2476
3058
|
collectionId,
|
|
2477
3059
|
transactionDriver,
|
|
@@ -2545,18 +3127,41 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2545
3127
|
})
|
|
2546
3128
|
}
|
|
2547
3129
|
|
|
2548
|
-
markIndexRemoved(
|
|
3130
|
+
markIndexRemoved(
|
|
3131
|
+
target: PersistedStorageTarget,
|
|
3132
|
+
signature: string,
|
|
3133
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3134
|
+
): Promise<void> {
|
|
2549
3135
|
return this.runRegular(() =>
|
|
2550
|
-
this.markIndexRemovedUnscheduled(
|
|
3136
|
+
this.markIndexRemovedUnscheduled(target, signature, ctx),
|
|
2551
3137
|
)
|
|
2552
3138
|
}
|
|
2553
3139
|
|
|
2554
3140
|
private async markIndexRemovedUnscheduled(
|
|
2555
|
-
|
|
3141
|
+
target: PersistedStorageTarget,
|
|
2556
3142
|
signature: string,
|
|
3143
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2557
3144
|
): Promise<void> {
|
|
2558
|
-
|
|
3145
|
+
const access = resolvePersistedStorageTarget(
|
|
3146
|
+
target,
|
|
3147
|
+
ctx?.cacheGenerationClaimId,
|
|
3148
|
+
)
|
|
3149
|
+
const collectionId = access.collectionId
|
|
3150
|
+
ctx = access.claimId
|
|
3151
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3152
|
+
: ctx
|
|
3153
|
+
await this.assertCacheGenerationReadClaim(
|
|
3154
|
+
this.driver,
|
|
3155
|
+
collectionId,
|
|
3156
|
+
ctx?.cacheGenerationClaimId,
|
|
3157
|
+
)
|
|
3158
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2559
3159
|
await this.runInTransaction(async (transactionDriver) => {
|
|
3160
|
+
await this.assertCacheGenerationReadClaim(
|
|
3161
|
+
transactionDriver,
|
|
3162
|
+
collectionId,
|
|
3163
|
+
ctx?.cacheGenerationClaimId,
|
|
3164
|
+
)
|
|
2560
3165
|
await this.assertCurrentSchemaVersion(
|
|
2561
3166
|
collectionId,
|
|
2562
3167
|
transactionDriver,
|
|
@@ -2588,23 +3193,109 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2588
3193
|
})
|
|
2589
3194
|
}
|
|
2590
3195
|
|
|
2591
|
-
|
|
3196
|
+
reserveLeadershipTerm(
|
|
3197
|
+
target: PersistedStorageTarget,
|
|
3198
|
+
observedTerm: number,
|
|
3199
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3200
|
+
): Promise<{
|
|
3201
|
+
latestTerm: number
|
|
3202
|
+
latestSeq: number
|
|
3203
|
+
latestRowVersion: number
|
|
3204
|
+
}> {
|
|
3205
|
+
// Election may run while hydration waits for its first writer route.
|
|
3206
|
+
return this.reserveLeadershipTermUnscheduled(target, observedTerm, ctx)
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
private async reserveLeadershipTermUnscheduled(
|
|
3210
|
+
target: PersistedStorageTarget,
|
|
3211
|
+
observedTerm: number,
|
|
3212
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3213
|
+
): Promise<{
|
|
3214
|
+
latestTerm: number
|
|
3215
|
+
latestSeq: number
|
|
3216
|
+
latestRowVersion: number
|
|
3217
|
+
}> {
|
|
3218
|
+
const access = resolvePersistedStorageTarget(
|
|
3219
|
+
target,
|
|
3220
|
+
ctx?.cacheGenerationClaimId,
|
|
3221
|
+
)
|
|
3222
|
+
const collectionId = access.collectionId
|
|
3223
|
+
ctx = access.claimId
|
|
3224
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3225
|
+
: ctx
|
|
3226
|
+
await this.assertCacheGenerationReadClaim(
|
|
3227
|
+
this.driver,
|
|
3228
|
+
collectionId,
|
|
3229
|
+
ctx?.cacheGenerationClaimId,
|
|
3230
|
+
)
|
|
3231
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
3232
|
+
return this.runInTransaction(async (transactionDriver) => {
|
|
3233
|
+
await this.assertCacheGenerationReadClaim(
|
|
3234
|
+
transactionDriver,
|
|
3235
|
+
collectionId,
|
|
3236
|
+
ctx?.cacheGenerationClaimId,
|
|
3237
|
+
)
|
|
3238
|
+
await this.assertCurrentSchemaVersion(
|
|
3239
|
+
collectionId,
|
|
3240
|
+
transactionDriver,
|
|
3241
|
+
`reserve a leadership term`,
|
|
3242
|
+
)
|
|
3243
|
+
await transactionDriver.run(
|
|
3244
|
+
`INSERT INTO leader_term (collection_id, latest_term)
|
|
3245
|
+
VALUES (?, ? + 1)
|
|
3246
|
+
ON CONFLICT(collection_id) DO UPDATE SET
|
|
3247
|
+
latest_term = MAX(leader_term.latest_term, ?) + 1`,
|
|
3248
|
+
[collectionId, observedTerm, observedTerm],
|
|
3249
|
+
)
|
|
3250
|
+
const [position, latestRowVersion] = await Promise.all([
|
|
3251
|
+
this.readStreamPosition(collectionId, transactionDriver),
|
|
3252
|
+
this.readLatestRowVersion(collectionId, transactionDriver),
|
|
3253
|
+
])
|
|
3254
|
+
return { ...position, latestRowVersion }
|
|
3255
|
+
})
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
getStreamPosition(
|
|
3259
|
+
target: PersistedStorageTarget,
|
|
3260
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3261
|
+
): Promise<{
|
|
2592
3262
|
latestTerm: number
|
|
2593
3263
|
latestSeq: number
|
|
2594
3264
|
latestRowVersion: number
|
|
2595
3265
|
}> {
|
|
2596
3266
|
// Election must not queue behind a hydrate awaiting its first writer route.
|
|
2597
3267
|
// The stream-position snapshot still uses the driver's transaction admission.
|
|
2598
|
-
return this.getStreamPositionUnscheduled(
|
|
3268
|
+
return this.getStreamPositionUnscheduled(target, ctx)
|
|
2599
3269
|
}
|
|
2600
3270
|
|
|
2601
|
-
private async getStreamPositionUnscheduled(
|
|
3271
|
+
private async getStreamPositionUnscheduled(
|
|
3272
|
+
target: PersistedStorageTarget,
|
|
3273
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
3274
|
+
): Promise<{
|
|
2602
3275
|
latestTerm: number
|
|
2603
3276
|
latestSeq: number
|
|
2604
3277
|
latestRowVersion: number
|
|
2605
3278
|
}> {
|
|
2606
|
-
|
|
3279
|
+
const access = resolvePersistedStorageTarget(
|
|
3280
|
+
target,
|
|
3281
|
+
ctx?.cacheGenerationClaimId,
|
|
3282
|
+
)
|
|
3283
|
+
const collectionId = access.collectionId
|
|
3284
|
+
ctx = access.claimId
|
|
3285
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3286
|
+
: ctx
|
|
3287
|
+
await this.assertCacheGenerationReadClaim(
|
|
3288
|
+
this.driver,
|
|
3289
|
+
collectionId,
|
|
3290
|
+
ctx?.cacheGenerationClaimId,
|
|
3291
|
+
)
|
|
3292
|
+
await this.ensureCollectionReady(collectionId, ctx?.cacheGenerationClaimId)
|
|
2607
3293
|
return this.runInTransaction(async (transactionDriver) => {
|
|
3294
|
+
await this.assertCacheGenerationReadClaim(
|
|
3295
|
+
transactionDriver,
|
|
3296
|
+
collectionId,
|
|
3297
|
+
ctx?.cacheGenerationClaimId,
|
|
3298
|
+
)
|
|
2608
3299
|
await this.assertCurrentSchemaVersion(
|
|
2609
3300
|
collectionId,
|
|
2610
3301
|
transactionDriver,
|
|
@@ -2701,23 +3392,46 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
2701
3392
|
}
|
|
2702
3393
|
|
|
2703
3394
|
pullSince(
|
|
2704
|
-
|
|
3395
|
+
target: PersistedStorageTarget,
|
|
2705
3396
|
fromRowVersion: number,
|
|
3397
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2706
3398
|
): Promise<SQLitePullSinceResult<string | number>> {
|
|
2707
3399
|
return this.runRegular(() =>
|
|
2708
|
-
this.pullSinceUnscheduled(
|
|
3400
|
+
this.pullSinceUnscheduled(target, fromRowVersion, ctx),
|
|
2709
3401
|
)
|
|
2710
3402
|
}
|
|
2711
3403
|
|
|
2712
3404
|
private async pullSinceUnscheduled(
|
|
2713
|
-
|
|
3405
|
+
target: PersistedStorageTarget,
|
|
2714
3406
|
fromRowVersion: number,
|
|
3407
|
+
ctx?: { cacheGenerationClaimId?: string },
|
|
2715
3408
|
): Promise<SQLitePullSinceResult<string | number>> {
|
|
2716
|
-
const
|
|
3409
|
+
const access = resolvePersistedStorageTarget(
|
|
3410
|
+
target,
|
|
3411
|
+
ctx?.cacheGenerationClaimId,
|
|
3412
|
+
)
|
|
3413
|
+
const collectionId = access.collectionId
|
|
3414
|
+
ctx = access.claimId
|
|
3415
|
+
? { ...ctx, cacheGenerationClaimId: access.claimId }
|
|
3416
|
+
: ctx
|
|
3417
|
+
await this.assertCacheGenerationReadClaim(
|
|
3418
|
+
this.driver,
|
|
3419
|
+
collectionId,
|
|
3420
|
+
ctx?.cacheGenerationClaimId,
|
|
3421
|
+
)
|
|
3422
|
+
const tableMapping = await this.ensureCollectionReady(
|
|
3423
|
+
collectionId,
|
|
3424
|
+
ctx?.cacheGenerationClaimId,
|
|
3425
|
+
)
|
|
2717
3426
|
const collectionTableSql = quoteIdentifier(tableMapping.tableName)
|
|
2718
3427
|
const tombstoneTableSql = quoteIdentifier(tableMapping.tombstoneTableName)
|
|
2719
3428
|
|
|
2720
3429
|
return this.runInTransaction(async (transactionDriver) => {
|
|
3430
|
+
await this.assertCacheGenerationReadClaim(
|
|
3431
|
+
transactionDriver,
|
|
3432
|
+
collectionId,
|
|
3433
|
+
ctx?.cacheGenerationClaimId,
|
|
3434
|
+
)
|
|
2721
3435
|
await this.assertCurrentSchemaVersion(
|
|
2722
3436
|
collectionId,
|
|
2723
3437
|
transactionDriver,
|
|
@@ -3042,6 +3756,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3042
3756
|
|
|
3043
3757
|
private async ensureCollectionReady(
|
|
3044
3758
|
collectionId: string,
|
|
3759
|
+
cacheGenerationClaimId?: string,
|
|
3045
3760
|
): Promise<CollectionTableMapping> {
|
|
3046
3761
|
await this.ensureInitialized()
|
|
3047
3762
|
|
|
@@ -3055,7 +3770,10 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3055
3770
|
return inFlight
|
|
3056
3771
|
}
|
|
3057
3772
|
|
|
3058
|
-
const loadPromise = this.ensureCollectionReadyInternal(
|
|
3773
|
+
const loadPromise = this.ensureCollectionReadyInternal(
|
|
3774
|
+
collectionId,
|
|
3775
|
+
cacheGenerationClaimId,
|
|
3776
|
+
)
|
|
3059
3777
|
this.collectionTableLoads.set(collectionId, loadPromise)
|
|
3060
3778
|
|
|
3061
3779
|
try {
|
|
@@ -3090,24 +3808,43 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3090
3808
|
|
|
3091
3809
|
private async ensureCollectionReadyInternal(
|
|
3092
3810
|
collectionId: string,
|
|
3811
|
+
cacheGenerationClaimId?: string,
|
|
3093
3812
|
): Promise<CollectionTableMapping> {
|
|
3813
|
+
// Record whether registration began for a managed physical ID. Collection
|
|
3814
|
+
// may remove that catalog row while this operation waits on external DDL.
|
|
3815
|
+
const managedAtAdmission =
|
|
3816
|
+
cacheGenerationClaimId !== undefined ||
|
|
3817
|
+
(hasCacheGenerationIdPrefix(collectionId) &&
|
|
3818
|
+
(
|
|
3819
|
+
await this.driver.query<{ physical_id: string }>(
|
|
3820
|
+
`SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
|
|
3821
|
+
[collectionId],
|
|
3822
|
+
)
|
|
3823
|
+
)[0] !== undefined)
|
|
3094
3824
|
let registration = await this.loadCollectionRegistration(collectionId)
|
|
3095
3825
|
|
|
3096
3826
|
if (!registration) {
|
|
3097
3827
|
const tableName = createPersistedTableName(collectionId, `c`)
|
|
3098
3828
|
const tombstoneTableName = createPersistedTableName(collectionId, `t`)
|
|
3099
|
-
await this.
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3829
|
+
await this.runInTransaction(async (transactionDriver) => {
|
|
3830
|
+
await this.assertCacheGenerationReadClaim(
|
|
3831
|
+
transactionDriver,
|
|
3832
|
+
collectionId,
|
|
3833
|
+
cacheGenerationClaimId,
|
|
3834
|
+
)
|
|
3835
|
+
await transactionDriver.run(
|
|
3836
|
+
`INSERT INTO collection_registry (
|
|
3837
|
+
collection_id,
|
|
3838
|
+
table_name,
|
|
3839
|
+
tombstone_table_name,
|
|
3840
|
+
schema_version,
|
|
3841
|
+
updated_at
|
|
3842
|
+
)
|
|
3843
|
+
VALUES (?, ?, ?, ?, CAST(strftime('%s', 'now') AS INTEGER))
|
|
3844
|
+
ON CONFLICT DO NOTHING`,
|
|
3845
|
+
[collectionId, tableName, tombstoneTableName, this.schemaVersion],
|
|
3846
|
+
)
|
|
3847
|
+
})
|
|
3111
3848
|
|
|
3112
3849
|
registration = await this.loadCollectionRegistration(collectionId)
|
|
3113
3850
|
if (!registration) {
|
|
@@ -3126,6 +3863,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3126
3863
|
this.schemaVersion,
|
|
3127
3864
|
tableName,
|
|
3128
3865
|
tombstoneTableName,
|
|
3866
|
+
cacheGenerationClaimId,
|
|
3129
3867
|
)
|
|
3130
3868
|
}
|
|
3131
3869
|
|
|
@@ -3174,6 +3912,38 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3174
3912
|
[collectionId],
|
|
3175
3913
|
)
|
|
3176
3914
|
await this.ensureCollectionKeyEvidenceTriggers(collectionId, tableName)
|
|
3915
|
+
if (managedAtAdmission) {
|
|
3916
|
+
// Registration is outside the read/write transaction. If collection
|
|
3917
|
+
// retired this generation while the DDL was in flight, remove any
|
|
3918
|
+
// empty table recreated after collection instead of caching it.
|
|
3919
|
+
const generation = await this.driver.query<{ physical_id: string }>(
|
|
3920
|
+
`SELECT physical_id FROM cache_generation WHERE physical_id = ?`,
|
|
3921
|
+
[collectionId],
|
|
3922
|
+
)
|
|
3923
|
+
if (!generation[0]) {
|
|
3924
|
+
await this.driver.exec(
|
|
3925
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}`,
|
|
3926
|
+
)
|
|
3927
|
+
await this.driver.exec(
|
|
3928
|
+
`DROP TABLE IF EXISTS ${quoteIdentifier(tombstoneTableName)}`,
|
|
3929
|
+
)
|
|
3930
|
+
await this.driver.run(
|
|
3931
|
+
`DELETE FROM collection_registry WHERE collection_id = ?`,
|
|
3932
|
+
[collectionId],
|
|
3933
|
+
)
|
|
3934
|
+
await this.driver.run(
|
|
3935
|
+
`DELETE FROM collection_version WHERE collection_id = ?`,
|
|
3936
|
+
[collectionId],
|
|
3937
|
+
)
|
|
3938
|
+
await this.driver.run(
|
|
3939
|
+
`DELETE FROM collection_reset_epoch WHERE collection_id = ?`,
|
|
3940
|
+
[collectionId],
|
|
3941
|
+
)
|
|
3942
|
+
throw new InvalidPersistedCollectionConfigError(
|
|
3943
|
+
`Persisted cache generation was collected before registration finished`,
|
|
3944
|
+
)
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3177
3947
|
const mapping = {
|
|
3178
3948
|
tableName,
|
|
3179
3949
|
tombstoneTableName,
|
|
@@ -3255,6 +4025,7 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3255
4025
|
nextSchemaVersion: number,
|
|
3256
4026
|
tableName: string,
|
|
3257
4027
|
tombstoneTableName: string,
|
|
4028
|
+
cacheGenerationClaimId?: string,
|
|
3258
4029
|
): Promise<void> {
|
|
3259
4030
|
if (this.schemaMismatchPolicy === `sync-absent-error`) {
|
|
3260
4031
|
throw new InvalidPersistedCollectionConfigError(
|
|
@@ -3267,6 +4038,11 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3267
4038
|
const tombstoneTableSql = quoteIdentifier(tombstoneTableName)
|
|
3268
4039
|
|
|
3269
4040
|
await this.runInTransaction(async (transactionDriver) => {
|
|
4041
|
+
await this.assertCacheGenerationReadClaim(
|
|
4042
|
+
transactionDriver,
|
|
4043
|
+
collectionId,
|
|
4044
|
+
cacheGenerationClaimId,
|
|
4045
|
+
)
|
|
3270
4046
|
const currentSchemaRows = await transactionDriver.query<{
|
|
3271
4047
|
schema_version: number
|
|
3272
4048
|
}>(
|
|
@@ -3361,6 +4137,49 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3361
4137
|
return
|
|
3362
4138
|
}
|
|
3363
4139
|
|
|
4140
|
+
await this.driver.exec(
|
|
4141
|
+
`CREATE TABLE IF NOT EXISTS cache_generation (
|
|
4142
|
+
logical_id TEXT NOT NULL,
|
|
4143
|
+
generation INTEGER NOT NULL,
|
|
4144
|
+
physical_id TEXT NOT NULL UNIQUE,
|
|
4145
|
+
retired INTEGER NOT NULL DEFAULT 0,
|
|
4146
|
+
PRIMARY KEY (logical_id, generation)
|
|
4147
|
+
)`,
|
|
4148
|
+
)
|
|
4149
|
+
await this.driver.exec(
|
|
4150
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS cache_generation_head
|
|
4151
|
+
ON cache_generation (logical_id) WHERE retired = 0`,
|
|
4152
|
+
)
|
|
4153
|
+
await this.driver.exec(
|
|
4154
|
+
`CREATE TABLE IF NOT EXISTS cache_generation_sequence (
|
|
4155
|
+
logical_id TEXT PRIMARY KEY,
|
|
4156
|
+
next_generation INTEGER NOT NULL
|
|
4157
|
+
)`,
|
|
4158
|
+
)
|
|
4159
|
+
await this.driver.exec(
|
|
4160
|
+
`CREATE TABLE IF NOT EXISTS cache_generation_claim (
|
|
4161
|
+
claim_id TEXT PRIMARY KEY,
|
|
4162
|
+
logical_id TEXT NOT NULL,
|
|
4163
|
+
physical_id TEXT NOT NULL,
|
|
4164
|
+
expires_at_ms INTEGER NOT NULL
|
|
4165
|
+
)`,
|
|
4166
|
+
)
|
|
4167
|
+
const claimColumns = await this.driver.query<{ name: string }>(
|
|
4168
|
+
`PRAGMA table_info(cache_generation_claim)`,
|
|
4169
|
+
)
|
|
4170
|
+
if (!claimColumns.some(({ name }) => name === `logical_id`)) {
|
|
4171
|
+
await this.driver.exec(
|
|
4172
|
+
`ALTER TABLE cache_generation_claim
|
|
4173
|
+
ADD COLUMN logical_id TEXT NOT NULL DEFAULT ''`,
|
|
4174
|
+
)
|
|
4175
|
+
}
|
|
4176
|
+
if (!claimColumns.some(({ name }) => name === `expires_at_ms`)) {
|
|
4177
|
+
await this.driver.exec(
|
|
4178
|
+
`ALTER TABLE cache_generation_claim
|
|
4179
|
+
ADD COLUMN expires_at_ms INTEGER NOT NULL DEFAULT 0`,
|
|
4180
|
+
)
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3364
4183
|
await this.driver.exec(
|
|
3365
4184
|
`CREATE TABLE IF NOT EXISTS collection_registry (
|
|
3366
4185
|
collection_id TEXT PRIMARY KEY,
|
|
@@ -3397,6 +4216,10 @@ export class SQLiteCorePersistenceAdapter implements PersistenceAdapter {
|
|
|
3397
4216
|
PRIMARY KEY (collection_id, term, seq)
|
|
3398
4217
|
)`,
|
|
3399
4218
|
)
|
|
4219
|
+
await this.driver.exec(
|
|
4220
|
+
`CREATE INDEX IF NOT EXISTS idx_applied_tx_collection_tx_id
|
|
4221
|
+
ON applied_tx (collection_id, tx_id)`,
|
|
4222
|
+
)
|
|
3400
4223
|
try {
|
|
3401
4224
|
await this.driver.exec(
|
|
3402
4225
|
`ALTER TABLE applied_tx ADD COLUMN replay_json TEXT`,
|