Initial phase 1 baseline implementation
This commit is contained in:
parent
099c53badf
commit
077cf7601a
29 changed files with 1339 additions and 39 deletions
|
|
@ -1,10 +1,10 @@
|
|||
-- Drive v2 uses forward-only migrations. Rollback is restore-or-fix-forward.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
CREATE TABLE drive_metadata (
|
||||
CREATE TABLE IF NOT EXISTS drive_metadata (
|
||||
singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (singleton),
|
||||
installed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO drive_metadata (singleton) VALUES (TRUE);
|
||||
|
||||
INSERT INTO drive_metadata (singleton) VALUES (TRUE)
|
||||
ON CONFLICT (singleton) DO NOTHING;
|
||||
387
db/migrations/000002_phase1_core.up.sql
Normal file
387
db/migrations/000002_phase1_core.up.sql
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
-- 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;
|
||||
9
db/migrations/migrations.go
Normal file
9
db/migrations/migrations.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Package migrations embeds the forward-only PostgreSQL migration source.
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
// Files contains every forward migration used by the application at startup.
|
||||
//
|
||||
//go:embed *.up.sql
|
||||
var Files embed.FS
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
-- name: DatabaseHealth :one
|
||||
SELECT 1::BIGINT AS result;
|
||||
|
||||
-- name: OwnerExists :one
|
||||
SELECT EXISTS (SELECT 1 FROM users)::BOOLEAN AS exists;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue