387 lines
17 KiB
SQL
387 lines
17 KiB
SQL
-- Phase 1 persistence foundation. UUIDv7 is provided by PostgreSQL 18.
|
|
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
username TEXT NOT NULL,
|
|
username_key TEXT NOT NULL,
|
|
display_name TEXT NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
root_node_id UUID,
|
|
setup_completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
password_changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT users_username_not_blank CHECK (btrim(username) <> ''),
|
|
CONSTRAINT users_username_key_not_blank CHECK (btrim(username_key) <> ''),
|
|
CONSTRAINT users_display_name_not_blank CHECK (btrim(display_name) <> ''),
|
|
CONSTRAINT users_username_key_unique UNIQUE (username_key)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX users_single_owner ON users ((TRUE));
|
|
|
|
CREATE TABLE totp_credentials (
|
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
encrypted_secret BYTEA NOT NULL,
|
|
enabled_at TIMESTAMPTZ,
|
|
verified_step BIGINT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT totp_secret_not_empty CHECK (octet_length(encrypted_secret) > 0),
|
|
CONSTRAINT totp_verified_step_nonnegative CHECK (verified_step IS NULL OR verified_step >= 0)
|
|
);
|
|
|
|
CREATE TABLE recovery_codes (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
code_hash BYTEA NOT NULL,
|
|
used_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT recovery_codes_hash_not_empty CHECK (octet_length(code_hash) > 0),
|
|
CONSTRAINT recovery_codes_hash_unique UNIQUE (user_id, code_hash)
|
|
);
|
|
|
|
CREATE TABLE client_instances (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
display_name TEXT NOT NULL,
|
|
client_type TEXT NOT NULL,
|
|
platform TEXT,
|
|
software_version TEXT,
|
|
protocol_version TEXT,
|
|
last_seen_at TIMESTAMPTZ,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT client_instances_display_name_not_blank CHECK (btrim(display_name) <> ''),
|
|
CONSTRAINT client_instances_type_not_blank CHECK (btrim(client_type) <> '')
|
|
);
|
|
|
|
CREATE INDEX client_instances_owner_active_idx
|
|
ON client_instances (owner_id, created_at DESC)
|
|
WHERE revoked_at IS NULL;
|
|
|
|
CREATE TABLE sessions (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
client_instance_id UUID REFERENCES client_instances(id) ON DELETE SET NULL,
|
|
token_hash BYTEA NOT NULL UNIQUE,
|
|
csrf_token_hash BYTEA NOT NULL,
|
|
remote_address INET,
|
|
user_agent TEXT,
|
|
authenticated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT sessions_token_hash_not_empty CHECK (octet_length(token_hash) > 0),
|
|
CONSTRAINT sessions_csrf_hash_not_empty CHECK (octet_length(csrf_token_hash) > 0),
|
|
CONSTRAINT sessions_expiry_after_creation CHECK (expires_at > created_at)
|
|
);
|
|
|
|
CREATE INDEX sessions_owner_active_idx
|
|
ON sessions (owner_id, expires_at)
|
|
WHERE revoked_at IS NULL;
|
|
|
|
CREATE TABLE api_tokens (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
client_instance_id UUID REFERENCES client_instances(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
token_hash BYTEA NOT NULL UNIQUE,
|
|
actions TEXT[] NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_used_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
revoked_at TIMESTAMPTZ,
|
|
CONSTRAINT api_tokens_name_not_blank CHECK (btrim(name) <> ''),
|
|
CONSTRAINT api_tokens_hash_not_empty CHECK (octet_length(token_hash) > 0),
|
|
CONSTRAINT api_tokens_actions_not_empty CHECK (cardinality(actions) > 0),
|
|
CONSTRAINT api_tokens_expiry_after_creation CHECK (expires_at IS NULL OR expires_at > created_at)
|
|
);
|
|
|
|
CREATE INDEX api_tokens_owner_active_idx
|
|
ON api_tokens (owner_id, created_at DESC)
|
|
WHERE revoked_at IS NULL;
|
|
|
|
CREATE TABLE blobs (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
object_id UUID NOT NULL DEFAULT uuidv7(),
|
|
sha256 BYTEA NOT NULL,
|
|
size_bytes BIGINT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
verified_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT blobs_object_id_unique UNIQUE (object_id),
|
|
CONSTRAINT blobs_owner_sha256_unique UNIQUE (owner_id, sha256),
|
|
CONSTRAINT blobs_sha256_length CHECK (octet_length(sha256) = 32),
|
|
CONSTRAINT blobs_size_nonnegative CHECK (size_bytes >= 0),
|
|
CONSTRAINT blobs_state_valid CHECK (state IN ('staged', 'available', 'unavailable')),
|
|
CONSTRAINT blobs_available_verified CHECK (state <> 'available' OR verified_at IS NOT NULL)
|
|
);
|
|
|
|
CREATE INDEX blobs_gc_candidates_idx ON blobs (created_at) WHERE state <> 'available';
|
|
|
|
CREATE TABLE nodes (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
parent_id UUID REFERENCES nodes(id) ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE,
|
|
name TEXT NOT NULL,
|
|
name_key TEXT NOT NULL,
|
|
node_type TEXT NOT NULL,
|
|
current_blob_id UUID REFERENCES blobs(id) ON DELETE RESTRICT,
|
|
size_bytes BIGINT NOT NULL DEFAULT 0,
|
|
mime_type TEXT,
|
|
node_revision BIGINT NOT NULL DEFAULT 1,
|
|
content_revision BIGINT NOT NULL DEFAULT 0,
|
|
content_modified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
metadata_updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
favorite BOOLEAN NOT NULL DEFAULT FALSE,
|
|
trash_root_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT nodes_name_not_blank CHECK (btrim(name) <> ''),
|
|
CONSTRAINT nodes_name_key_not_blank CHECK (btrim(name_key) <> ''),
|
|
CONSTRAINT nodes_type_valid CHECK (node_type IN ('directory', 'file')),
|
|
CONSTRAINT nodes_file_blob_shape CHECK (
|
|
(node_type = 'directory' AND current_blob_id IS NULL AND size_bytes = 0 AND content_revision = 0)
|
|
OR (node_type = 'file' AND current_blob_id IS NOT NULL)
|
|
),
|
|
CONSTRAINT nodes_size_nonnegative CHECK (size_bytes >= 0),
|
|
CONSTRAINT nodes_node_revision_positive CHECK (node_revision > 0),
|
|
CONSTRAINT nodes_content_revision_nonnegative CHECK (content_revision >= 0),
|
|
CONSTRAINT nodes_not_own_parent CHECK (parent_id IS NULL OR parent_id <> id)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX nodes_sibling_name_unique
|
|
ON nodes (owner_id, parent_id, name_key) NULLS NOT DISTINCT;
|
|
CREATE INDEX nodes_parent_listing_idx ON nodes (owner_id, parent_id, name_key, id);
|
|
CREATE INDEX nodes_current_blob_idx ON nodes (current_blob_id) WHERE current_blob_id IS NOT NULL;
|
|
CREATE INDEX nodes_trash_roots_idx ON nodes (owner_id, trash_root_at) WHERE trash_root_at IS NOT NULL;
|
|
CREATE INDEX nodes_name_trgm_idx ON nodes USING gin (name gin_trgm_ops);
|
|
|
|
ALTER TABLE users
|
|
ADD CONSTRAINT users_root_node_fk
|
|
FOREIGN KEY (root_node_id) REFERENCES nodes(id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
|
|
|
|
CREATE TABLE credential_root_grants (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
|
|
api_token_id UUID REFERENCES api_tokens(id) ON DELETE CASCADE,
|
|
root_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
actions TEXT[] NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT credential_root_grants_one_credential CHECK (num_nonnulls(session_id, api_token_id) = 1),
|
|
CONSTRAINT credential_root_grants_actions_not_empty CHECK (cardinality(actions) > 0)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX credential_root_grants_session_unique
|
|
ON credential_root_grants (session_id, root_node_id) WHERE session_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX credential_root_grants_token_unique
|
|
ON credential_root_grants (api_token_id, root_node_id) WHERE api_token_id IS NOT NULL;
|
|
|
|
CREATE TABLE idempotency_records (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
credential_kind TEXT NOT NULL,
|
|
credential_id UUID NOT NULL,
|
|
client_instance_id UUID REFERENCES client_instances(id) ON DELETE SET NULL,
|
|
idempotency_key TEXT NOT NULL,
|
|
request_fingerprint BYTEA NOT NULL,
|
|
operation_type TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
response_reference JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
completed_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
CONSTRAINT idempotency_key_not_blank CHECK (btrim(idempotency_key) <> ''),
|
|
CONSTRAINT idempotency_fingerprint_not_empty CHECK (octet_length(request_fingerprint) > 0),
|
|
CONSTRAINT idempotency_operation_not_blank CHECK (btrim(operation_type) <> ''),
|
|
CONSTRAINT idempotency_credential_kind_valid CHECK (credential_kind IN ('session', 'api_token', 'internal')),
|
|
CONSTRAINT idempotency_state_valid CHECK (state IN ('in_progress', 'completed', 'failed')),
|
|
CONSTRAINT idempotency_expiry_after_creation CHECK (expires_at > created_at),
|
|
CONSTRAINT idempotency_scope_unique UNIQUE (owner_id, credential_kind, credential_id, idempotency_key)
|
|
);
|
|
|
|
CREATE INDEX idempotency_records_expiry_idx ON idempotency_records (expires_at);
|
|
|
|
CREATE TABLE changes (
|
|
sequence BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
event_id UUID NOT NULL DEFAULT uuidv7() UNIQUE,
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
event_type TEXT NOT NULL,
|
|
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
node_id UUID NOT NULL,
|
|
node_revision BIGINT,
|
|
content_revision BIGINT,
|
|
subtree_effect BOOLEAN NOT NULL DEFAULT FALSE,
|
|
resulting_state JSONB,
|
|
tombstone JSONB,
|
|
previous_state JSONB,
|
|
mutation_origin JSONB NOT NULL,
|
|
committed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT changes_event_type_not_blank CHECK (btrim(event_type) <> ''),
|
|
CONSTRAINT changes_schema_version_positive CHECK (schema_version > 0),
|
|
CONSTRAINT changes_node_revision_positive CHECK (node_revision IS NULL OR node_revision > 0),
|
|
CONSTRAINT changes_content_revision_nonnegative CHECK (content_revision IS NULL OR content_revision >= 0),
|
|
CONSTRAINT changes_result_shape CHECK (num_nonnulls(resulting_state, tombstone) = 1)
|
|
);
|
|
|
|
CREATE INDEX changes_owner_sequence_idx ON changes (owner_id, sequence);
|
|
CREATE INDEX changes_node_history_idx ON changes (owner_id, node_id, sequence DESC);
|
|
|
|
CREATE TABLE replacement_recovery (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
|
blob_id UUID NOT NULL REFERENCES blobs(id) ON DELETE RESTRICT,
|
|
replaced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
restored_at TIMESTAMPTZ,
|
|
CONSTRAINT replacement_recovery_expiry_after_replace CHECK (expires_at > replaced_at)
|
|
);
|
|
|
|
CREATE INDEX replacement_recovery_active_idx
|
|
ON replacement_recovery (owner_id, expires_at)
|
|
WHERE restored_at IS NULL;
|
|
CREATE INDEX replacement_recovery_blob_idx ON replacement_recovery (blob_id);
|
|
|
|
CREATE TABLE commit_intents (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
operation_id UUID NOT NULL,
|
|
node_id UUID REFERENCES nodes(id) ON DELETE RESTRICT,
|
|
parent_id UUID REFERENCES nodes(id) ON DELETE RESTRICT,
|
|
expected_node_revision BIGINT,
|
|
target_name TEXT NOT NULL,
|
|
target_name_key TEXT NOT NULL,
|
|
object_id UUID NOT NULL,
|
|
sha256 BYTEA NOT NULL,
|
|
size_bytes BIGINT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
promoted_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
failure_code TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT commit_intents_operation_unique UNIQUE (owner_id, operation_id),
|
|
CONSTRAINT commit_intents_target_name_not_blank CHECK (btrim(target_name) <> ''),
|
|
CONSTRAINT commit_intents_target_key_not_blank CHECK (btrim(target_name_key) <> ''),
|
|
CONSTRAINT commit_intents_expected_revision_positive CHECK (expected_node_revision IS NULL OR expected_node_revision > 0),
|
|
CONSTRAINT commit_intents_sha256_length CHECK (octet_length(sha256) = 32),
|
|
CONSTRAINT commit_intents_size_nonnegative CHECK (size_bytes >= 0),
|
|
CONSTRAINT commit_intents_state_valid CHECK (state IN ('pending', 'promoted', 'completed', 'failed'))
|
|
);
|
|
|
|
CREATE INDEX commit_intents_recovery_idx
|
|
ON commit_intents (state, created_at)
|
|
WHERE state IN ('pending', 'promoted');
|
|
|
|
CREATE TABLE resource_leases (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
purpose TEXT NOT NULL,
|
|
resource_kind TEXT NOT NULL,
|
|
resource_id UUID NOT NULL,
|
|
holder_id UUID NOT NULL,
|
|
acquired_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
heartbeat_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
released_at TIMESTAMPTZ,
|
|
CONSTRAINT resource_leases_purpose_not_blank CHECK (btrim(purpose) <> ''),
|
|
CONSTRAINT resource_leases_kind_not_blank CHECK (btrim(resource_kind) <> ''),
|
|
CONSTRAINT resource_leases_expiry_after_acquire CHECK (expires_at > acquired_at)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX resource_leases_active_unique
|
|
ON resource_leases (owner_id, purpose, resource_kind, resource_id)
|
|
WHERE released_at IS NULL;
|
|
CREATE INDEX resource_leases_expiry_idx
|
|
ON resource_leases (expires_at)
|
|
WHERE released_at IS NULL;
|
|
|
|
CREATE TABLE jobs (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
job_type TEXT NOT NULL,
|
|
state TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
progress JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
attempt_count BIGINT NOT NULL DEFAULT 0,
|
|
max_attempts BIGINT NOT NULL DEFAULT 5,
|
|
lease_holder UUID,
|
|
lease_expires_at TIMESTAMPTZ,
|
|
heartbeat_at TIMESTAMPTZ,
|
|
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
cancel_requested_at TIMESTAMPTZ,
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
last_error_code TEXT,
|
|
last_error_detail TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT jobs_type_not_blank CHECK (btrim(job_type) <> ''),
|
|
CONSTRAINT jobs_state_valid CHECK (state IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')),
|
|
CONSTRAINT jobs_attempt_count_nonnegative CHECK (attempt_count >= 0),
|
|
CONSTRAINT jobs_max_attempts_positive CHECK (max_attempts > 0)
|
|
);
|
|
|
|
CREATE INDEX jobs_claim_idx ON jobs (next_attempt_at, created_at)
|
|
WHERE state = 'queued';
|
|
CREATE INDEX jobs_lease_idx ON jobs (lease_expires_at)
|
|
WHERE state = 'running';
|
|
|
|
CREATE TABLE settings (
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
setting_key TEXT NOT NULL,
|
|
value JSONB NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (owner_id, setting_key),
|
|
CONSTRAINT settings_key_not_blank CHECK (btrim(setting_key) <> '')
|
|
);
|
|
|
|
CREATE TABLE audit_events (
|
|
sequence BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
event_id UUID NOT NULL DEFAULT uuidv7() UNIQUE,
|
|
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
actor JSONB NOT NULL,
|
|
action TEXT NOT NULL,
|
|
resource_type TEXT,
|
|
resource_id UUID,
|
|
outcome TEXT NOT NULL,
|
|
request_id UUID,
|
|
operation_id UUID,
|
|
details JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT audit_events_action_not_blank CHECK (btrim(action) <> ''),
|
|
CONSTRAINT audit_events_outcome_valid CHECK (outcome IN ('succeeded', 'denied', 'failed'))
|
|
);
|
|
|
|
CREATE INDEX audit_events_owner_time_idx ON audit_events (owner_id, occurred_at DESC);
|
|
CREATE INDEX audit_events_resource_idx ON audit_events (resource_type, resource_id, occurred_at DESC);
|
|
|
|
CREATE TABLE export_leases (
|
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
operation_id UUID NOT NULL,
|
|
state TEXT NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
released_at TIMESTAMPTZ,
|
|
CONSTRAINT export_leases_operation_unique UNIQUE (owner_id, operation_id),
|
|
CONSTRAINT export_leases_state_valid CHECK (state IN ('active', 'released', 'expired')),
|
|
CONSTRAINT export_leases_expiry_after_creation CHECK (expires_at > created_at)
|
|
);
|
|
|
|
CREATE TABLE export_items (
|
|
export_lease_id UUID NOT NULL REFERENCES export_leases(id) ON DELETE CASCADE,
|
|
node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,
|
|
blob_id UUID REFERENCES blobs(id) ON DELETE RESTRICT,
|
|
node_revision BIGINT NOT NULL,
|
|
relative_path TEXT NOT NULL,
|
|
PRIMARY KEY (export_lease_id, node_id),
|
|
CONSTRAINT export_items_node_revision_positive CHECK (node_revision > 0),
|
|
CONSTRAINT export_items_relative_path_not_blank CHECK (btrim(relative_path) <> '')
|
|
);
|
|
|
|
CREATE INDEX export_items_blob_idx ON export_items (blob_id) WHERE blob_id IS NOT NULL;
|