Files
ELY-Browser/cloudflare/migrations/0012_sync_snapshot_head.sql
T

283 lines
9.5 KiB
SQL

ALTER TABLE sync_snapshots
ADD COLUMN head_revision INTEGER NOT NULL DEFAULT 0 CHECK (head_revision >= 0);
ALTER TABLE sync_snapshots
ADD COLUMN base_head_revision INTEGER CHECK (base_head_revision IS NULL OR base_head_revision >= 1);
ALTER TABLE sync_snapshots
ADD COLUMN base_snapshot_id TEXT;
ALTER TABLE sync_snapshots
ADD COLUMN base_payload_hash TEXT
CHECK (
base_payload_hash IS NULL
OR (
length(base_payload_hash) = 64
AND base_payload_hash NOT GLOB '*[^0-9a-f]*'
)
);
ALTER TABLE sync_snapshot_encryption RENAME TO sync_snapshot_encryption_v1;
DROP INDEX IF EXISTS idx_sync_snapshots_encrypted_latest;
CREATE TABLE sync_snapshot_encryption (
user_id TEXT NOT NULL,
snapshot_id TEXT NOT NULL,
encryption_version INTEGER NOT NULL CHECK (encryption_version IN (1, 2)),
vault_generation INTEGER NOT NULL CHECK (vault_generation >= 1),
key_id TEXT NOT NULL,
content_hash TEXT NOT NULL,
PRIMARY KEY (user_id, snapshot_id),
FOREIGN KEY (user_id, snapshot_id) REFERENCES sync_snapshots (user_id, snapshot_id)
);
INSERT INTO sync_snapshot_encryption (
user_id,
snapshot_id,
encryption_version,
vault_generation,
key_id,
content_hash
)
SELECT
user_id,
snapshot_id,
encryption_version,
vault_generation,
key_id,
content_hash
FROM sync_snapshot_encryption_v1;
DROP TABLE sync_snapshot_encryption_v1;
CREATE INDEX idx_sync_snapshots_encrypted_latest
ON sync_snapshot_encryption (user_id, encryption_version, snapshot_id);
UPDATE sync_snapshots AS candidate
SET head_revision = 1
WHERE EXISTS (
SELECT 1
FROM sync_snapshot_encryption AS encryption
WHERE encryption.user_id = candidate.user_id
AND encryption.snapshot_id = candidate.snapshot_id
AND encryption.encryption_version = 1
)
AND NOT EXISTS (
SELECT 1
FROM sync_snapshots AS newer
INNER JOIN sync_snapshot_encryption AS newer_encryption
ON newer_encryption.user_id = newer.user_id
AND newer_encryption.snapshot_id = newer.snapshot_id
AND newer_encryption.encryption_version = 1
WHERE newer.user_id = candidate.user_id
AND (
newer.created_at > candidate.created_at
OR (
newer.created_at = candidate.created_at
AND newer.snapshot_id < candidate.snapshot_id
)
)
);
CREATE UNIQUE INDEX idx_sync_snapshot_committed_revision
ON sync_snapshots (user_id, head_revision)
WHERE head_revision > 0;
CREATE TABLE sync_snapshot_heads (
user_id TEXT NOT NULL PRIMARY KEY,
head_revision INTEGER NOT NULL CHECK (head_revision >= 1),
snapshot_id TEXT NOT NULL,
payload_hash TEXT NOT NULL
CHECK (length(payload_hash) = 64 AND payload_hash NOT GLOB '*[^0-9a-f]*'),
updated_at INTEGER NOT NULL CHECK (updated_at >= 0),
FOREIGN KEY (user_id, snapshot_id)
REFERENCES sync_snapshots (user_id, snapshot_id) ON DELETE RESTRICT,
FOREIGN KEY (user_id, snapshot_id)
REFERENCES sync_snapshot_encryption (user_id, snapshot_id) ON DELETE RESTRICT
);
INSERT INTO sync_snapshot_heads (
user_id,
head_revision,
snapshot_id,
payload_hash,
updated_at
)
SELECT user_id, 1, snapshot_id, payload_hash, created_at
FROM sync_snapshots
WHERE head_revision = 1;
CREATE TRIGGER sync_snapshot_candidate_insert_guard
BEFORE INSERT ON sync_snapshots
FOR EACH ROW
WHEN NEW.head_revision > 0
BEGIN
SELECT CASE WHEN (
(
NEW.head_revision = 1
AND NEW.base_head_revision IS NULL
AND NEW.base_snapshot_id IS NULL
AND NEW.base_payload_hash IS NULL
AND NOT EXISTS (
SELECT 1 FROM sync_snapshot_heads WHERE user_id = NEW.user_id
)
)
OR
(
NEW.head_revision > 1
AND NEW.base_head_revision IS NOT NULL
AND NEW.base_snapshot_id IS NOT NULL
AND NEW.base_payload_hash IS NOT NULL
AND EXISTS (
SELECT 1
FROM sync_snapshot_heads AS head
INNER JOIN sync_snapshots AS base
ON base.user_id = head.user_id
AND base.snapshot_id = head.snapshot_id
AND base.payload_hash = head.payload_hash
WHERE head.user_id = NEW.user_id
AND head.head_revision = NEW.base_head_revision
AND head.snapshot_id = NEW.base_snapshot_id
AND head.payload_hash = NEW.base_payload_hash
AND NEW.head_revision = head.head_revision + 1
AND NEW.logical_clock > base.logical_clock
)
)
) THEN 1 ELSE RAISE(ABORT, 'sync_snapshot_head_cas_failed') END;
END;
CREATE TRIGGER sync_snapshot_candidate_update_guard
BEFORE UPDATE ON sync_snapshots
FOR EACH ROW
WHEN OLD.head_revision > 0 OR NEW.head_revision > 0
BEGIN
SELECT CASE WHEN (
(
NEW.head_revision = 1
AND NEW.base_head_revision IS NULL
AND NEW.base_snapshot_id IS NULL
AND NEW.base_payload_hash IS NULL
AND NOT EXISTS (
SELECT 1 FROM sync_snapshot_heads WHERE user_id = NEW.user_id
)
)
OR
(
NEW.head_revision > 1
AND NEW.base_head_revision IS NOT NULL
AND NEW.base_snapshot_id IS NOT NULL
AND NEW.base_payload_hash IS NOT NULL
AND EXISTS (
SELECT 1
FROM sync_snapshot_heads AS head
INNER JOIN sync_snapshots AS base
ON base.user_id = head.user_id
AND base.snapshot_id = head.snapshot_id
AND base.payload_hash = head.payload_hash
WHERE head.user_id = NEW.user_id
AND head.head_revision = NEW.base_head_revision
AND head.snapshot_id = NEW.base_snapshot_id
AND head.payload_hash = NEW.base_payload_hash
AND NEW.head_revision = head.head_revision + 1
AND NEW.logical_clock > base.logical_clock
)
)
) THEN 1 ELSE RAISE(ABORT, 'sync_snapshot_head_cas_failed') END;
END;
CREATE TRIGGER sync_snapshot_head_insert_guard
BEFORE INSERT ON sync_snapshot_heads
FOR EACH ROW
BEGIN
SELECT CASE WHEN EXISTS (
SELECT 1
FROM sync_snapshots AS snapshot
INNER JOIN sync_snapshot_encryption AS encryption
ON encryption.user_id = snapshot.user_id
AND encryption.snapshot_id = snapshot.snapshot_id
INNER JOIN sync_vault_accounts AS account
ON account.user_id = snapshot.user_id
AND account.current_key_id = encryption.key_id
AND account.current_generation = encryption.vault_generation
INNER JOIN user_devices AS device
ON device.user_id = snapshot.user_id
AND device.device_id = snapshot.device_id
AND device.approval_status = 'approved'
AND device.revoked_at IS NULL
INNER JOIN user_device_keys AS keys
ON keys.user_id = device.user_id
AND keys.device_id = device.device_id
AND keys.key_protocol_version = 2
AND keys.wrapping_public_key IS NOT NULL
WHERE snapshot.user_id = NEW.user_id
AND snapshot.snapshot_id = NEW.snapshot_id
AND snapshot.payload_hash = NEW.payload_hash
AND snapshot.head_revision = NEW.head_revision
AND snapshot.head_revision = 1
AND snapshot.base_head_revision IS NULL
AND snapshot.base_snapshot_id IS NULL
AND snapshot.base_payload_hash IS NULL
AND encryption.encryption_version = 2
AND NOT EXISTS (
SELECT 1
FROM sync_vault_rotation_r2_objects AS staged
INNER JOIN sync_vault_rotations AS rotation
ON rotation.user_id = staged.user_id
AND rotation.idempotency_key = staged.rotation_idempotency_key
WHERE staged.user_id = snapshot.user_id
AND staged.r2_key = snapshot.r2_key
AND rotation.cleanup_started_at IS NOT NULL
)
) THEN 1 ELSE RAISE(ABORT, 'sync_snapshot_head_cas_failed') END;
END;
CREATE TRIGGER sync_snapshot_head_update_guard
BEFORE UPDATE ON sync_snapshot_heads
FOR EACH ROW
BEGIN
SELECT CASE WHEN (
NEW.user_id = OLD.user_id
AND NEW.head_revision = OLD.head_revision + 1
AND EXISTS (
SELECT 1
FROM sync_snapshots AS snapshot
INNER JOIN sync_snapshot_encryption AS encryption
ON encryption.user_id = snapshot.user_id
AND encryption.snapshot_id = snapshot.snapshot_id
INNER JOIN sync_vault_accounts AS account
ON account.user_id = snapshot.user_id
AND account.current_key_id = encryption.key_id
AND account.current_generation = encryption.vault_generation
INNER JOIN user_devices AS device
ON device.user_id = snapshot.user_id
AND device.device_id = snapshot.device_id
AND device.approval_status = 'approved'
AND device.revoked_at IS NULL
INNER JOIN user_device_keys AS keys
ON keys.user_id = device.user_id
AND keys.device_id = device.device_id
AND keys.key_protocol_version = 2
AND keys.wrapping_public_key IS NOT NULL
WHERE snapshot.user_id = NEW.user_id
AND snapshot.snapshot_id = NEW.snapshot_id
AND snapshot.payload_hash = NEW.payload_hash
AND snapshot.head_revision = NEW.head_revision
AND snapshot.base_head_revision = OLD.head_revision
AND snapshot.base_snapshot_id = OLD.snapshot_id
AND snapshot.base_payload_hash = OLD.payload_hash
AND encryption.encryption_version = 2
AND NOT EXISTS (
SELECT 1
FROM sync_vault_rotation_r2_objects AS staged
INNER JOIN sync_vault_rotations AS rotation
ON rotation.user_id = staged.user_id
AND rotation.idempotency_key = staged.rotation_idempotency_key
WHERE staged.user_id = snapshot.user_id
AND staged.r2_key = snapshot.r2_key
AND rotation.cleanup_started_at IS NOT NULL
)
)
) THEN 1 ELSE RAISE(ABORT, 'sync_snapshot_head_cas_failed') END;
END;