283 lines
9.5 KiB
SQL
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;
|