chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS platform_metadata;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
CREATE TABLE platform_metadata (
|
||||
key varchar(100) PRIMARY KEY,
|
||||
value jsonb NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO platform_metadata (key, value)
|
||||
VALUES ('schema_baseline', '{"version":1,"description":"initial platform schema"}'::jsonb);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,7 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS outbox_event;
|
||||
DROP TABLE IF EXISTS idempotency_record;
|
||||
DROP TABLE IF EXISTS audit_event;
|
||||
DROP TABLE IF EXISTS user_account;
|
||||
DROP TABLE IF EXISTS tenant;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,82 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE tenant (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
name varchar(200) NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'closed')),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz
|
||||
);
|
||||
|
||||
CREATE TABLE user_account (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
identity_subject varchar(255) NOT NULL,
|
||||
display_name varchar(120),
|
||||
status varchar(32) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'deleted')),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
UNIQUE (tenant_id, identity_subject)
|
||||
);
|
||||
|
||||
CREATE TABLE audit_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid REFERENCES tenant(id),
|
||||
actor_type varchar(32) NOT NULL,
|
||||
actor_public_id uuid,
|
||||
action varchar(120) NOT NULL,
|
||||
resource_type varchar(80) NOT NULL,
|
||||
resource_id uuid,
|
||||
request_id varchar(128),
|
||||
purpose varchar(255),
|
||||
result varchar(32) NOT NULL CHECK (result IN ('success', 'failure', 'denied')),
|
||||
detail jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
occurred_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX audit_event_tenant_time_idx ON audit_event (tenant_id, occurred_at DESC);
|
||||
CREATE INDEX audit_event_request_idx ON audit_event (request_id) WHERE request_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE idempotency_record (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid REFERENCES tenant(id),
|
||||
scope varchar(120) NOT NULL,
|
||||
idempotency_key varchar(255) NOT NULL,
|
||||
request_hash char(64) NOT NULL,
|
||||
state varchar(24) NOT NULL CHECK (state IN ('processing', 'completed', 'failed')),
|
||||
response_status integer,
|
||||
response_body jsonb,
|
||||
resource_id uuid,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant_id, scope, idempotency_key)
|
||||
);
|
||||
CREATE INDEX idempotency_record_expiry_idx ON idempotency_record (expires_at);
|
||||
|
||||
CREATE TABLE outbox_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid REFERENCES tenant(id),
|
||||
aggregate_type varchar(80) NOT NULL,
|
||||
aggregate_id uuid NOT NULL,
|
||||
event_type varchar(120) NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'dead')),
|
||||
attempt_count integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
||||
available_at timestamptz NOT NULL DEFAULT now(),
|
||||
locked_at timestamptz,
|
||||
locked_by varchar(120),
|
||||
last_error_code varchar(120),
|
||||
completed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX outbox_event_claim_idx ON outbox_event (status, available_at, created_at)
|
||||
WHERE status IN ('pending', 'processing');
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,16 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS member_invitation;
|
||||
DROP TABLE IF EXISTS household_member;
|
||||
DROP TABLE IF EXISTS household;
|
||||
DROP INDEX IF EXISTS user_account_oidc_identity_idx;
|
||||
ALTER TABLE user_account
|
||||
DROP CONSTRAINT IF EXISTS user_account_id_tenant_key,
|
||||
DROP CONSTRAINT IF EXISTS user_account_tenant_oidc_identity_key,
|
||||
DROP COLUMN IF EXISTS last_login_at,
|
||||
DROP COLUMN IF EXISTS oidc_issuer;
|
||||
ALTER TABLE user_account RENAME COLUMN oidc_subject TO identity_subject;
|
||||
ALTER TABLE user_account
|
||||
ADD CONSTRAINT user_account_tenant_id_identity_subject_key
|
||||
UNIQUE (tenant_id, identity_subject);
|
||||
ALTER TABLE tenant DROP COLUMN IF EXISTS policy_version;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,99 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE tenant
|
||||
ADD COLUMN policy_version varchar(64) NOT NULL DEFAULT 'local-v1';
|
||||
|
||||
ALTER TABLE user_account
|
||||
RENAME COLUMN identity_subject TO oidc_subject;
|
||||
|
||||
ALTER TABLE user_account
|
||||
ADD COLUMN oidc_issuer varchar(300) NOT NULL DEFAULT 'urn:legacy:local',
|
||||
ADD COLUMN last_login_at timestamptz;
|
||||
|
||||
ALTER TABLE user_account
|
||||
DROP CONSTRAINT user_account_tenant_id_identity_subject_key;
|
||||
|
||||
ALTER TABLE user_account
|
||||
ADD CONSTRAINT user_account_tenant_oidc_identity_key
|
||||
UNIQUE (tenant_id, oidc_issuer, oidc_subject),
|
||||
ADD CONSTRAINT user_account_id_tenant_key UNIQUE (id, tenant_id);
|
||||
|
||||
CREATE INDEX user_account_oidc_identity_idx
|
||||
ON user_account (oidc_issuer, oidc_subject)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE TABLE household (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
name varchar(200) NOT NULL,
|
||||
owner_user_id uuid NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'archived', 'deleted')),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
CONSTRAINT household_owner_same_tenant_fk
|
||||
FOREIGN KEY (owner_user_id, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT household_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX household_tenant_status_idx ON household (tenant_id, status);
|
||||
|
||||
CREATE TABLE household_member (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
role varchar(32) NOT NULL
|
||||
CHECK (role IN ('owner', 'admin', 'editor', 'viewer')),
|
||||
status varchar(32) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'removed')),
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
removed_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT household_member_household_tenant_fk
|
||||
FOREIGN KEY (household_id, tenant_id)
|
||||
REFERENCES household (id, tenant_id),
|
||||
CONSTRAINT household_member_user_tenant_fk
|
||||
FOREIGN KEY (user_id, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id)
|
||||
);
|
||||
CREATE UNIQUE INDEX household_member_active_user_key
|
||||
ON household_member (household_id, user_id)
|
||||
WHERE status = 'active';
|
||||
CREATE INDEX household_member_tenant_user_status_idx
|
||||
ON household_member (tenant_id, user_id, status);
|
||||
CREATE INDEX household_member_household_status_idx
|
||||
ON household_member (household_id, status);
|
||||
|
||||
CREATE TABLE member_invitation (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
invitee_hint varchar(255),
|
||||
token_digest char(64) NOT NULL UNIQUE,
|
||||
role varchar(32) NOT NULL
|
||||
CHECK (role IN ('admin', 'editor', 'viewer')),
|
||||
expires_at timestamptz NOT NULL,
|
||||
used_at timestamptz,
|
||||
revoked_at timestamptz,
|
||||
created_by uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT member_invitation_household_tenant_fk
|
||||
FOREIGN KEY (household_id, tenant_id)
|
||||
REFERENCES household (id, tenant_id),
|
||||
CONSTRAINT member_invitation_creator_tenant_fk
|
||||
FOREIGN KEY (created_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX member_invitation_household_expiry_idx
|
||||
ON member_invitation (household_id, expires_at)
|
||||
WHERE used_at IS NULL AND revoked_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
DROP INDEX IF EXISTS outbox_event_stale_lock_idx;
|
||||
DROP INDEX IF EXISTS outbox_event_deduplication_key;
|
||||
ALTER TABLE outbox_event
|
||||
DROP COLUMN IF EXISTS dead_at,
|
||||
DROP COLUMN IF EXISTS payload_hash,
|
||||
DROP COLUMN IF EXISTS deduplication_key;
|
||||
DROP INDEX IF EXISTS idempotency_record_tenant_scope_idx;
|
||||
ALTER TABLE idempotency_record
|
||||
DROP CONSTRAINT IF EXISTS idempotency_record_actor_scope_key,
|
||||
DROP COLUMN IF EXISTS actor_key;
|
||||
ALTER TABLE idempotency_record
|
||||
ADD CONSTRAINT idempotency_record_tenant_id_scope_idempotency_key_key
|
||||
UNIQUE (tenant_id, scope, idempotency_key);
|
||||
COMMIT;
|
||||
@@ -0,0 +1,33 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE idempotency_record
|
||||
ADD COLUMN actor_key varchar(500) NOT NULL DEFAULT 'urn:legacy:unknown';
|
||||
|
||||
ALTER TABLE idempotency_record
|
||||
DROP CONSTRAINT idempotency_record_tenant_id_scope_idempotency_key_key;
|
||||
|
||||
ALTER TABLE idempotency_record
|
||||
ADD CONSTRAINT idempotency_record_actor_scope_key
|
||||
UNIQUE (actor_key, scope, idempotency_key);
|
||||
|
||||
CREATE INDEX idempotency_record_tenant_scope_idx
|
||||
ON idempotency_record (tenant_id, scope, created_at DESC);
|
||||
|
||||
ALTER TABLE outbox_event
|
||||
ADD COLUMN deduplication_key varchar(255),
|
||||
ADD COLUMN payload_hash char(64) NOT NULL DEFAULT repeat('0', 64),
|
||||
ADD COLUMN dead_at timestamptz;
|
||||
|
||||
CREATE UNIQUE INDEX outbox_event_deduplication_key
|
||||
ON outbox_event (
|
||||
COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
||||
event_type,
|
||||
deduplication_key
|
||||
)
|
||||
WHERE deduplication_key IS NOT NULL;
|
||||
|
||||
CREATE INDEX outbox_event_stale_lock_idx
|
||||
ON outbox_event (locked_at)
|
||||
WHERE status = 'processing';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,14 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS provider_event;
|
||||
DROP TABLE IF EXISTS rtc_task;
|
||||
DROP TABLE IF EXISTS conversation_state_history;
|
||||
DROP TABLE IF EXISTS conversation;
|
||||
DROP TABLE IF EXISTS context_snapshot;
|
||||
DROP TABLE IF EXISTS consent_snapshot;
|
||||
DROP TABLE IF EXISTS device_twin;
|
||||
DROP TABLE IF EXISTS device_binding;
|
||||
DROP TABLE IF EXISTS device;
|
||||
DROP TABLE IF EXISTS subject_profile;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,214 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE subject_profile (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
preferred_name varchar(120) NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'inactive', 'deleted')),
|
||||
profile_version bigint NOT NULL DEFAULT 1 CHECK (profile_version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
CONSTRAINT subject_household_tenant_fk FOREIGN KEY (household_id, tenant_id)
|
||||
REFERENCES household (id, tenant_id),
|
||||
CONSTRAINT subject_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX subject_tenant_household_status_idx ON subject_profile (tenant_id, household_id, status);
|
||||
|
||||
CREATE TABLE device (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid REFERENCES tenant(id),
|
||||
serial_digest char(64) NOT NULL UNIQUE,
|
||||
model varchar(120) NOT NULL,
|
||||
hardware_revision varchar(80),
|
||||
status varchar(24) NOT NULL DEFAULT 'registered'
|
||||
CHECK (status IN ('registered', 'quarantined', 'revoked')),
|
||||
credential_ref varchar(500) NOT NULL,
|
||||
minimum_security_version varchar(80),
|
||||
registered_at timestamptz NOT NULL DEFAULT now(),
|
||||
revoked_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX device_tenant_status_idx ON device (tenant_id, status);
|
||||
|
||||
CREATE TABLE device_binding (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
household_id uuid NOT NULL,
|
||||
subject_id uuid NOT NULL,
|
||||
bound_at timestamptz NOT NULL DEFAULT now(),
|
||||
unbound_at timestamptz,
|
||||
bound_by uuid,
|
||||
unbound_by uuid,
|
||||
reason varchar(255),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_binding_device_tenant_fk FOREIGN KEY (device_id, tenant_id)
|
||||
REFERENCES device (id, tenant_id),
|
||||
CONSTRAINT device_binding_household_tenant_fk FOREIGN KEY (household_id, tenant_id)
|
||||
REFERENCES household (id, tenant_id),
|
||||
CONSTRAINT device_binding_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT device_binding_bound_by_tenant_fk FOREIGN KEY (bound_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT device_binding_unbound_by_tenant_fk FOREIGN KEY (unbound_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id)
|
||||
);
|
||||
CREATE UNIQUE INDEX device_binding_current_device_key ON device_binding (device_id) WHERE unbound_at IS NULL;
|
||||
CREATE INDEX device_binding_tenant_subject_idx ON device_binding (tenant_id, subject_id) WHERE unbound_at IS NULL;
|
||||
|
||||
CREATE TABLE device_twin (
|
||||
device_id uuid PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
desired_revision bigint NOT NULL DEFAULT 1 CHECK (desired_revision > 0),
|
||||
desired_config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
reported_revision bigint,
|
||||
reported_config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
firmware_version varchar(80),
|
||||
board_revision varchar(80),
|
||||
rtc_sdk_version varchar(80),
|
||||
capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
last_reported_at timestamptz,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_twin_device_tenant_fk FOREIGN KEY (device_id, tenant_id)
|
||||
REFERENCES device (id, tenant_id)
|
||||
);
|
||||
|
||||
CREATE TABLE consent_snapshot (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
snapshot_version bigint NOT NULL CHECK (snapshot_version > 0),
|
||||
effective_consents jsonb NOT NULL,
|
||||
source_consent_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT consent_snapshot_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT consent_snapshot_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
|
||||
CREATE TABLE context_snapshot (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
profile_version bigint NOT NULL CHECK (profile_version > 0),
|
||||
consent_snapshot_id uuid NOT NULL,
|
||||
memory_versions jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||||
prompt_bundle_version varchar(80) NOT NULL,
|
||||
engine_mode varchar(32) NOT NULL CHECK (engine_mode IN ('s2s_pure', 's2s_hybrid', 'cascade')),
|
||||
content_digest char(64) NOT NULL,
|
||||
sanitized_summary varchar(1000),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT context_snapshot_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT context_snapshot_consent_tenant_fk FOREIGN KEY (consent_snapshot_id, tenant_id)
|
||||
REFERENCES consent_snapshot (id, tenant_id),
|
||||
CONSTRAINT context_snapshot_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
|
||||
CREATE TABLE conversation (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
subject_id uuid NOT NULL,
|
||||
device_id uuid NOT NULL,
|
||||
consent_snapshot_id uuid NOT NULL,
|
||||
context_snapshot_id uuid NOT NULL,
|
||||
provider_task_ref varchar(200) NOT NULL UNIQUE,
|
||||
engine_mode varchar(32) NOT NULL CHECK (engine_mode IN ('s2s_pure', 's2s_hybrid', 'cascade')),
|
||||
retention_mode varchar(32) NOT NULL CHECK (retention_mode IN ('none', 'transcript_only', 'transcript_and_audio')),
|
||||
status varchar(32) NOT NULL CHECK (status IN ('PREPARING', 'ROOM_ISSUED', 'STARTING_AGENT', 'ACTIVE', 'STOPPING', 'COMPLETED', 'FAILED', 'EXPIRED')),
|
||||
allocation_expires_at timestamptz NOT NULL,
|
||||
lease_expires_at timestamptz,
|
||||
last_heartbeat_seq bigint NOT NULL DEFAULT 0 CHECK (last_heartbeat_seq >= 0),
|
||||
started_at timestamptz,
|
||||
ended_at timestamptz,
|
||||
stop_reason varchar(64),
|
||||
failure_code varchar(64),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT conversation_household_tenant_fk FOREIGN KEY (household_id, tenant_id)
|
||||
REFERENCES household (id, tenant_id),
|
||||
CONSTRAINT conversation_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT conversation_device_tenant_fk FOREIGN KEY (device_id, tenant_id)
|
||||
REFERENCES device (id, tenant_id),
|
||||
CONSTRAINT conversation_consent_tenant_fk FOREIGN KEY (consent_snapshot_id, tenant_id)
|
||||
REFERENCES consent_snapshot (id, tenant_id),
|
||||
CONSTRAINT conversation_context_tenant_fk FOREIGN KEY (context_snapshot_id, tenant_id)
|
||||
REFERENCES context_snapshot (id, tenant_id),
|
||||
CONSTRAINT conversation_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX conversation_tenant_subject_time_idx ON conversation (tenant_id, subject_id, created_at DESC);
|
||||
CREATE INDEX conversation_device_status_idx ON conversation (device_id, status);
|
||||
CREATE UNIQUE INDEX conversation_one_active_per_device ON conversation (device_id)
|
||||
WHERE status NOT IN ('COMPLETED', 'FAILED', 'EXPIRED');
|
||||
|
||||
CREATE TABLE conversation_state_history (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
conversation_id uuid NOT NULL REFERENCES conversation(id),
|
||||
from_status varchar(32),
|
||||
to_status varchar(32) NOT NULL,
|
||||
reason varchar(255),
|
||||
source varchar(40) NOT NULL CHECK (source IN ('device', 'platform', 'provider', 'worker', 'operator')),
|
||||
provider_event_id uuid,
|
||||
request_id varchar(128),
|
||||
occurred_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX conversation_history_time_idx ON conversation_state_history (conversation_id, occurred_at, id);
|
||||
|
||||
CREATE TABLE rtc_task (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
conversation_id uuid NOT NULL UNIQUE REFERENCES conversation(id),
|
||||
provider varchar(40) NOT NULL,
|
||||
provider_task_ref varchar(200) NOT NULL UNIQUE,
|
||||
room_id varchar(200) NOT NULL UNIQUE,
|
||||
device_rtc_user_id varchar(200) NOT NULL,
|
||||
agent_rtc_user_id varchar(200) NOT NULL,
|
||||
status varchar(32) NOT NULL CHECK (status IN ('ALLOCATED', 'STARTING', 'ACTIVE', 'STOPPING', 'COMPLETED', 'FAILED', 'UNKNOWN')),
|
||||
lease_expires_at timestamptz,
|
||||
start_attempts integer NOT NULL DEFAULT 0 CHECK (start_attempts >= 0),
|
||||
stop_attempts integer NOT NULL DEFAULT 0 CHECK (stop_attempts >= 0),
|
||||
last_checked_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT rtc_distinct_users CHECK (device_rtc_user_id <> agent_rtc_user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE provider_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
conversation_id uuid NOT NULL,
|
||||
provider varchar(40) NOT NULL,
|
||||
provider_event_key varchar(255) NOT NULL,
|
||||
provider_task_ref varchar(200) NOT NULL,
|
||||
event_type varchar(100) NOT NULL,
|
||||
occurred_at timestamptz NOT NULL,
|
||||
received_at timestamptz NOT NULL DEFAULT now(),
|
||||
payload_digest char(64) NOT NULL,
|
||||
sanitized_payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
processing_status varchar(24) NOT NULL DEFAULT 'pending'
|
||||
CHECK (processing_status IN ('pending', 'processed', 'ignored', 'failed')),
|
||||
processed_at timestamptz,
|
||||
last_error varchar(500),
|
||||
CONSTRAINT provider_event_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT provider_event_key UNIQUE (provider, provider_event_key)
|
||||
);
|
||||
CREATE INDEX provider_event_conversation_time_idx ON provider_event (conversation_id, received_at);
|
||||
CREATE INDEX provider_event_pending_idx ON provider_event (processing_status, received_at)
|
||||
WHERE processing_status IN ('pending', 'failed');
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,24 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS consent_record;
|
||||
DROP TABLE IF EXISTS subject_fact;
|
||||
|
||||
ALTER TABLE subject_profile
|
||||
DROP CONSTRAINT IF EXISTS subject_profile_created_by_tenant_fk,
|
||||
DROP CONSTRAINT IF EXISTS subject_profile_updated_by_tenant_fk,
|
||||
DROP CONSTRAINT IF EXISTS subject_profile_status_check;
|
||||
|
||||
UPDATE subject_profile SET status='inactive' WHERE status='suspended';
|
||||
UPDATE subject_profile SET status='deleted' WHERE status='deleting';
|
||||
|
||||
ALTER TABLE subject_profile
|
||||
DROP COLUMN IF EXISTS language,
|
||||
DROP COLUMN IF EXISTS timezone,
|
||||
DROP COLUMN IF EXISTS profile_data,
|
||||
DROP COLUMN IF EXISTS conversation_preferences,
|
||||
DROP COLUMN IF EXISTS created_by,
|
||||
DROP COLUMN IF EXISTS updated_by,
|
||||
ADD CONSTRAINT subject_profile_status_check
|
||||
CHECK (status IN ('active', 'inactive', 'deleted'));
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,91 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE subject_profile
|
||||
DROP CONSTRAINT IF EXISTS subject_profile_status_check;
|
||||
|
||||
UPDATE subject_profile SET status='suspended' WHERE status='inactive';
|
||||
|
||||
ALTER TABLE subject_profile
|
||||
ADD COLUMN language varchar(32) NOT NULL DEFAULT 'zh-CN',
|
||||
ADD COLUMN timezone varchar(64) NOT NULL DEFAULT 'Asia/Shanghai',
|
||||
ADD COLUMN profile_data jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN conversation_preferences jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN created_by uuid,
|
||||
ADD COLUMN updated_by uuid,
|
||||
ADD CONSTRAINT subject_profile_status_check
|
||||
CHECK (status IN ('active', 'suspended', 'deleting', 'deleted')),
|
||||
ADD CONSTRAINT subject_profile_created_by_tenant_fk
|
||||
FOREIGN KEY (created_by, tenant_id) REFERENCES user_account (id, tenant_id),
|
||||
ADD CONSTRAINT subject_profile_updated_by_tenant_fk
|
||||
FOREIGN KEY (updated_by, tenant_id) REFERENCES user_account (id, tenant_id);
|
||||
|
||||
CREATE TABLE subject_fact (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
fact_type varchar(64) NOT NULL,
|
||||
content text NOT NULL,
|
||||
source_type varchar(40) NOT NULL
|
||||
CHECK (source_type IN ('subject_statement', 'family_entry', 'model_candidate', 'imported')),
|
||||
source_ref varchar(300),
|
||||
confirmation_status varchar(32) NOT NULL DEFAULT 'pending'
|
||||
CHECK (confirmation_status IN ('pending', 'confirmed', 'rejected', 'superseded')),
|
||||
sensitivity varchar(24) NOT NULL DEFAULT 'normal'
|
||||
CHECK (sensitivity IN ('normal', 'sensitive', 'high')),
|
||||
valid_from timestamptz,
|
||||
valid_to timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_by uuid NOT NULL,
|
||||
updated_by uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT subject_fact_subject_tenant_fk
|
||||
FOREIGN KEY (subject_id, tenant_id) REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT subject_fact_created_by_tenant_fk
|
||||
FOREIGN KEY (created_by, tenant_id) REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT subject_fact_updated_by_tenant_fk
|
||||
FOREIGN KEY (updated_by, tenant_id) REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT subject_fact_valid_range CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from),
|
||||
CONSTRAINT subject_fact_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX subject_fact_subject_status_idx
|
||||
ON subject_fact (tenant_id, subject_id, confirmation_status, updated_at DESC);
|
||||
|
||||
CREATE TABLE consent_record (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
consent_type varchar(64) NOT NULL
|
||||
CHECK (consent_type IN (
|
||||
'transcript_storage', 'raw_audio_storage', 'story_extraction',
|
||||
'personalized_memory', 'family_sharing'
|
||||
)),
|
||||
scope jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
policy_version varchar(64) NOT NULL,
|
||||
notice_digest varchar(128) NOT NULL,
|
||||
actor_user_id uuid NOT NULL,
|
||||
evidence_ref varchar(300),
|
||||
granted_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz,
|
||||
withdrawn_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT consent_record_subject_tenant_fk
|
||||
FOREIGN KEY (subject_id, tenant_id) REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT consent_record_actor_tenant_fk
|
||||
FOREIGN KEY (actor_user_id, tenant_id) REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT consent_record_expiry_check CHECK (expires_at IS NULL OR expires_at > granted_at),
|
||||
CONSTRAINT consent_record_withdrawal_check CHECK (withdrawn_at IS NULL OR withdrawn_at >= granted_at),
|
||||
CONSTRAINT consent_record_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX consent_record_subject_type_time_idx
|
||||
ON consent_record (tenant_id, subject_id, consent_type, granted_at DESC, id DESC);
|
||||
|
||||
-- “同一类型最多一条当前有效授权”包含对当前时间的判断,不能由使用
|
||||
-- now() 的部分唯一索引表达。授予服务必须在锁定 subject_profile 后,于同一
|
||||
-- 事务中检查 withdrawn_at / expires_at 并追加新记录。
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,3 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS device_claim;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,27 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE device_claim (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
token_digest char(64) NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz,
|
||||
used_at timestamptz,
|
||||
used_binding_id uuid REFERENCES device_binding(id),
|
||||
failed_attempts integer NOT NULL DEFAULT 0 CHECK (failed_attempts >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_claim_device_tenant_fk
|
||||
FOREIGN KEY (device_id, tenant_id) REFERENCES device(id, tenant_id),
|
||||
CONSTRAINT device_claim_use_consistency CHECK (
|
||||
(used_at IS NULL AND used_binding_id IS NULL) OR
|
||||
(used_at IS NOT NULL AND used_binding_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX device_claim_device_status_idx
|
||||
ON device_claim (tenant_id, device_id, expires_at DESC);
|
||||
CREATE UNIQUE INDEX device_claim_one_unused_per_device
|
||||
ON device_claim (device_id) WHERE used_at IS NULL AND revoked_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS memory_index_entry;
|
||||
ALTER TABLE curated_memory DROP CONSTRAINT IF EXISTS curated_memory_current_version_fk;
|
||||
DROP TABLE IF EXISTS memory_version;
|
||||
DROP TABLE IF EXISTS curated_memory;
|
||||
DROP TABLE IF EXISTS fact_candidate;
|
||||
DROP TABLE IF EXISTS story_source;
|
||||
ALTER TABLE story_draft DROP CONSTRAINT IF EXISTS story_draft_current_revision_fk;
|
||||
DROP TABLE IF EXISTS story_revision;
|
||||
DROP TABLE IF EXISTS story_draft;
|
||||
DROP TABLE IF EXISTS conversation_summary;
|
||||
DROP TABLE IF EXISTS conversation_turn;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,204 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE conversation_turn (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
conversation_id uuid NOT NULL,
|
||||
turn_key varchar(255) NOT NULL,
|
||||
sequence_no bigint NOT NULL CHECK (sequence_no > 0),
|
||||
speaker varchar(24) NOT NULL CHECK (speaker IN ('subject', 'agent')),
|
||||
start_at timestamptz NOT NULL,
|
||||
end_at timestamptz NOT NULL,
|
||||
final_text text NOT NULL CHECK (length(final_text) BETWEEN 1 AND 20000),
|
||||
interrupted boolean NOT NULL DEFAULT false,
|
||||
provider_ref varchar(255),
|
||||
source_type varchar(24) NOT NULL CHECK (source_type IN ('provider', 'device')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT conversation_turn_time_order CHECK (end_at >= start_at),
|
||||
CONSTRAINT conversation_turn_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT conversation_turn_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT conversation_turn_id_tenant_key UNIQUE (id, tenant_id),
|
||||
CONSTRAINT conversation_turn_business_key UNIQUE (conversation_id, turn_key),
|
||||
CONSTRAINT conversation_turn_sequence_key UNIQUE (conversation_id, sequence_no)
|
||||
);
|
||||
CREATE INDEX conversation_turn_subject_time_idx ON conversation_turn (tenant_id, subject_id, start_at DESC);
|
||||
CREATE INDEX conversation_turn_conversation_page_idx ON conversation_turn (conversation_id, sequence_no, id);
|
||||
|
||||
CREATE TABLE conversation_summary (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
conversation_id uuid NOT NULL,
|
||||
summary_type varchar(32) NOT NULL CHECK (summary_type IN ('conversation_summary', 'open_topic')),
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 20000),
|
||||
source_turn_ids jsonb NOT NULL CHECK (jsonb_typeof(source_turn_ids)='array' AND jsonb_array_length(source_turn_ids)>0),
|
||||
model_ref varchar(160) NOT NULL,
|
||||
prompt_version varchar(80) NOT NULL,
|
||||
generation_version bigint NOT NULL CHECK (generation_version > 0),
|
||||
confidence numeric(5,4) CHECK (confidence BETWEEN 0 AND 1),
|
||||
status varchar(24) NOT NULL DEFAULT 'READY' CHECK (status IN ('PROCESSING', 'READY', 'FAILED', 'SUPERSEDED')),
|
||||
last_error varchar(500),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT conversation_summary_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT conversation_summary_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT conversation_summary_generation_key UNIQUE (conversation_id, summary_type, generation_version)
|
||||
);
|
||||
CREATE INDEX conversation_summary_conversation_idx ON conversation_summary (conversation_id, summary_type, generation_version DESC);
|
||||
|
||||
CREATE TABLE story_draft (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
conversation_id uuid NOT NULL,
|
||||
title varchar(300) NOT NULL CHECK (length(title)>0),
|
||||
status varchar(24) NOT NULL DEFAULT 'CANDIDATE'
|
||||
CHECK (status IN ('CANDIDATE', 'IN_REVIEW', 'CONFIRMED', 'REJECTED', 'SUPERSEDED')),
|
||||
confidence numeric(5,4) CHECK (confidence BETWEEN 0 AND 1),
|
||||
sensitivity varchar(24) NOT NULL DEFAULT 'normal' CHECK (sensitivity IN ('normal', 'sensitive', 'high')),
|
||||
generation_version bigint NOT NULL CHECK (generation_version > 0),
|
||||
current_revision_id uuid,
|
||||
reviewed_by uuid,
|
||||
reviewed_at timestamptz,
|
||||
review_reason varchar(500),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT story_draft_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT story_draft_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT story_draft_reviewer_tenant_fk FOREIGN KEY (reviewed_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT story_draft_generation_key UNIQUE (conversation_id, generation_version)
|
||||
);
|
||||
CREATE INDEX story_draft_subject_status_idx ON story_draft (tenant_id, subject_id, status, created_at DESC);
|
||||
|
||||
CREATE TABLE story_revision (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
story_id uuid NOT NULL REFERENCES story_draft(id),
|
||||
revision_no bigint NOT NULL CHECK (revision_no > 0),
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 50000),
|
||||
author_type varchar(24) NOT NULL CHECK (author_type IN ('model', 'user')),
|
||||
author_id uuid,
|
||||
edit_reason varchar(500),
|
||||
base_revision_id uuid REFERENCES story_revision(id),
|
||||
model_ref varchar(160),
|
||||
prompt_version varchar(80),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT story_revision_number_key UNIQUE (story_id, revision_no)
|
||||
);
|
||||
ALTER TABLE story_draft ADD CONSTRAINT story_draft_current_revision_fk
|
||||
FOREIGN KEY (current_revision_id) REFERENCES story_revision(id);
|
||||
|
||||
CREATE TABLE story_source (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
story_revision_id uuid NOT NULL REFERENCES story_revision(id) ON DELETE CASCADE,
|
||||
conversation_turn_id uuid NOT NULL REFERENCES conversation_turn(id),
|
||||
source_start_offset integer CHECK (source_start_offset >= 0),
|
||||
source_end_offset integer CHECK (source_end_offset >= source_start_offset),
|
||||
source_role varchar(24) NOT NULL CHECK (source_role IN ('subject', 'agent')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT story_source_turn_key UNIQUE (story_revision_id, conversation_turn_id, source_start_offset, source_end_offset)
|
||||
);
|
||||
CREATE INDEX story_source_turn_idx ON story_source (conversation_turn_id);
|
||||
|
||||
CREATE TABLE fact_candidate (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
story_id uuid NOT NULL REFERENCES story_draft(id),
|
||||
fact_type varchar(64) NOT NULL,
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 4000),
|
||||
statement_basis varchar(24) NOT NULL CHECK (statement_basis IN ('subject_statement', 'model_inference')),
|
||||
time_range jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
confidence numeric(5,4) CHECK (confidence BETWEEN 0 AND 1),
|
||||
sensitivity varchar(24) NOT NULL DEFAULT 'normal' CHECK (sensitivity IN ('normal', 'sensitive', 'high')),
|
||||
source_turn_ids jsonb NOT NULL CHECK (jsonb_typeof(source_turn_ids)='array' AND jsonb_array_length(source_turn_ids)>0),
|
||||
status varchar(24) NOT NULL DEFAULT 'CANDIDATE'
|
||||
CHECK (status IN ('CANDIDATE', 'CONFIRMED', 'REJECTED', 'SUPERSEDED')),
|
||||
generation_version bigint NOT NULL CHECK (generation_version > 0),
|
||||
reviewed_by uuid,
|
||||
reviewed_at timestamptz,
|
||||
review_reason varchar(500),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT fact_candidate_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT fact_candidate_reviewer_tenant_fk FOREIGN KEY (reviewed_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX fact_candidate_story_status_idx ON fact_candidate (story_id, status);
|
||||
|
||||
CREATE TABLE curated_memory (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
current_version_id uuid,
|
||||
status varchar(24) NOT NULL DEFAULT 'ACTIVE'
|
||||
CHECK (status IN ('ACTIVE', 'DISABLED', 'SUPERSEDED', 'PENDING_DELETE', 'DELETED')),
|
||||
sensitivity varchar(24) NOT NULL DEFAULT 'normal' CHECK (sensitivity IN ('normal', 'sensitive', 'high')),
|
||||
valid_from timestamptz,
|
||||
valid_to timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz,
|
||||
CONSTRAINT curated_memory_valid_range CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from),
|
||||
CONSTRAINT curated_memory_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX curated_memory_subject_status_idx ON curated_memory (tenant_id, subject_id, status, updated_at DESC);
|
||||
|
||||
CREATE TABLE memory_version (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
memory_id uuid NOT NULL REFERENCES curated_memory(id),
|
||||
version_no bigint NOT NULL CHECK (version_no > 0),
|
||||
content text NOT NULL CHECK (length(content) BETWEEN 1 AND 50000),
|
||||
source_refs jsonb NOT NULL CHECK (jsonb_typeof(source_refs)='array' AND jsonb_array_length(source_refs)>0),
|
||||
confirmed_by uuid NOT NULL REFERENCES user_account(id),
|
||||
confirmed_at timestamptz NOT NULL DEFAULT now(),
|
||||
change_reason varchar(500),
|
||||
content_digest char(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT memory_version_number_key UNIQUE (memory_id, version_no)
|
||||
);
|
||||
ALTER TABLE curated_memory ADD CONSTRAINT curated_memory_current_version_fk
|
||||
FOREIGN KEY (current_version_id) REFERENCES memory_version(id);
|
||||
|
||||
CREATE TABLE memory_index_entry (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
memory_id uuid NOT NULL REFERENCES curated_memory(id),
|
||||
memory_version_id uuid NOT NULL REFERENCES memory_version(id),
|
||||
index_type varchar(24) NOT NULL CHECK (index_type IN ('pgvector', 'provider')),
|
||||
embedding vector(1536),
|
||||
provider_ref varchar(300),
|
||||
status varchar(24) NOT NULL DEFAULT 'PENDING'
|
||||
CHECK (status IN ('PENDING', 'ACTIVE', 'STALE', 'FAILED', 'DELETED')),
|
||||
indexed_at timestamptz,
|
||||
last_attempt_at timestamptz,
|
||||
last_error varchar(500),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT memory_index_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT memory_index_version_key UNIQUE (memory_version_id, index_type)
|
||||
);
|
||||
CREATE INDEX memory_index_scope_idx ON memory_index_entry (tenant_id, subject_id, status, memory_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,7 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS tombstone;
|
||||
DROP TABLE IF EXISTS privacy_subtask;
|
||||
DROP TABLE IF EXISTS privacy_request;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,69 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE privacy_request (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
request_type varchar(32) NOT NULL
|
||||
CHECK (request_type IN ('ACCESS', 'CORRECTION', 'CONSENT_WITHDRAWAL', 'DELETE')),
|
||||
scope jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(scope)='object'),
|
||||
requested_by uuid NOT NULL,
|
||||
status varchar(32) NOT NULL DEFAULT 'SUBMITTED'
|
||||
CHECK (status IN ('SUBMITTED', 'VERIFYING', 'EXECUTING', 'COMPLETED', 'PARTIAL_FAILED')),
|
||||
deadline_at timestamptz,
|
||||
legal_hold boolean NOT NULL DEFAULT false,
|
||||
result_summary jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(result_summary)='object'),
|
||||
idempotency_key varchar(255) NOT NULL,
|
||||
request_digest char(64) NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz,
|
||||
CONSTRAINT privacy_request_subject_tenant_fk
|
||||
FOREIGN KEY (subject_id, tenant_id) REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT privacy_request_requester_tenant_fk
|
||||
FOREIGN KEY (requested_by, tenant_id) REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT privacy_request_actor_idempotency_key
|
||||
UNIQUE (tenant_id, requested_by, idempotency_key)
|
||||
);
|
||||
CREATE INDEX privacy_request_subject_time_idx
|
||||
ON privacy_request (tenant_id, subject_id, created_at DESC, id DESC);
|
||||
CREATE INDEX privacy_request_work_idx
|
||||
ON privacy_request (status, updated_at) WHERE status IN ('SUBMITTED', 'VERIFYING', 'EXECUTING', 'PARTIAL_FAILED');
|
||||
|
||||
CREATE TABLE privacy_subtask (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
privacy_request_id uuid NOT NULL REFERENCES privacy_request(id) ON DELETE CASCADE,
|
||||
subsystem varchar(32) NOT NULL
|
||||
CHECK (subsystem IN ('postgresql', 'redis', 'object_storage', 'pgvector', 'provider_index', 'export_files')),
|
||||
target_type varchar(64) NOT NULL,
|
||||
target_ref varchar(300) NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'PENDING'
|
||||
CHECK (status IN ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED')),
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
next_attempt_at timestamptz,
|
||||
last_error varchar(500),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz,
|
||||
CONSTRAINT privacy_subtask_target_key
|
||||
UNIQUE (privacy_request_id, subsystem, target_type, target_ref)
|
||||
);
|
||||
CREATE INDEX privacy_subtask_work_idx
|
||||
ON privacy_subtask (status, next_attempt_at, created_at)
|
||||
WHERE status IN ('PENDING', 'RUNNING', 'FAILED');
|
||||
|
||||
CREATE TABLE tombstone (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
resource_type varchar(80) NOT NULL,
|
||||
resource_digest char(64) NOT NULL,
|
||||
deleted_at timestamptz NOT NULL DEFAULT now(),
|
||||
deletion_request_id uuid NOT NULL REFERENCES privacy_request(id),
|
||||
retention_reason varchar(500) NOT NULL,
|
||||
CONSTRAINT tombstone_resource_key UNIQUE (tenant_id, resource_type, resource_digest)
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,5 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS export_task;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,39 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE export_task (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
requested_by uuid NOT NULL,
|
||||
scope jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(scope)='object'),
|
||||
status varchar(24) NOT NULL DEFAULT 'QUEUED'
|
||||
CHECK (status IN ('QUEUED', 'RUNNING', 'READY', 'FAILED', 'EXPIRED')),
|
||||
object_ref varchar(700),
|
||||
object_size bigint CHECK (object_size IS NULL OR object_size >= 0),
|
||||
content_digest char(64),
|
||||
expires_at timestamptz,
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
last_error varchar(500),
|
||||
idempotency_key varchar(255) NOT NULL,
|
||||
request_digest char(64) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz,
|
||||
CONSTRAINT export_task_subject_tenant_fk
|
||||
FOREIGN KEY (subject_id, tenant_id) REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT export_task_requester_tenant_fk
|
||||
FOREIGN KEY (requested_by, tenant_id) REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT export_task_actor_idempotency_key
|
||||
UNIQUE (tenant_id, requested_by, idempotency_key),
|
||||
CONSTRAINT export_task_ready_fields CHECK (
|
||||
status <> 'READY' OR
|
||||
(object_ref IS NOT NULL AND object_size IS NOT NULL AND content_digest IS NOT NULL AND expires_at IS NOT NULL AND completed_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX export_task_subject_time_idx
|
||||
ON export_task (tenant_id, subject_id, created_at DESC, id DESC);
|
||||
CREATE INDEX export_task_expiry_idx
|
||||
ON export_task (expires_at) WHERE status='READY';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,3 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS device_command;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,33 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE device_command (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
command_type varchar(40) NOT NULL CHECK (command_type IN ('apply_config','reboot','diagnostics','firmware_update')),
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(payload)='object'),
|
||||
status varchar(24) NOT NULL DEFAULT 'QUEUED'
|
||||
CHECK (status IN ('QUEUED','DELIVERED','ACKNOWLEDGED','SUCCEEDED','FAILED','EXPIRED','CANCELLED')),
|
||||
result_summary jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(result_summary)='object'),
|
||||
requested_by uuid NOT NULL,
|
||||
idempotency_key varchar(255) NOT NULL,
|
||||
request_digest char(64) NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
delivery_attempts integer NOT NULL DEFAULT 0 CHECK (delivery_attempts >= 0),
|
||||
delivered_at timestamptz,
|
||||
acknowledged_at timestamptz,
|
||||
completed_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_command_device_tenant_fk FOREIGN KEY (device_id,tenant_id) REFERENCES device(id,tenant_id),
|
||||
CONSTRAINT device_command_requester_tenant_fk FOREIGN KEY (requested_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT device_command_actor_idempotency_key UNIQUE (tenant_id,requested_by,idempotency_key)
|
||||
);
|
||||
CREATE INDEX device_command_delivery_idx ON device_command(device_id,status,created_at)
|
||||
WHERE status IN ('QUEUED','DELIVERED','ACKNOWLEDGED');
|
||||
CREATE INDEX device_command_expiry_idx ON device_command(expires_at)
|
||||
WHERE status IN ('QUEUED','DELIVERED','ACKNOWLEDGED');
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS firmware_assignment;
|
||||
DROP TABLE IF EXISTS firmware_rollout;
|
||||
DROP TABLE IF EXISTS firmware_release;
|
||||
DROP TABLE IF EXISTS firmware_signing_key;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,80 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE firmware_signing_key (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
key_id varchar(120) NOT NULL,
|
||||
public_key_base64 varchar(100) NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'ACTIVE' CHECK(status IN ('ACTIVE','REVOKED')),
|
||||
created_by uuid NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
revoked_at timestamptz,
|
||||
CONSTRAINT firmware_key_creator_tenant_fk FOREIGN KEY(created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT firmware_key_tenant_key UNIQUE(tenant_id,key_id)
|
||||
);
|
||||
|
||||
CREATE TABLE firmware_release (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
model varchar(120) NOT NULL,
|
||||
hardware_revision varchar(80),
|
||||
version varchar(80) NOT NULL,
|
||||
object_ref varchar(700) NOT NULL,
|
||||
expected_size bigint NOT NULL CHECK (expected_size BETWEEN 1 AND 268435456),
|
||||
expected_digest char(64) NOT NULL,
|
||||
signature_algorithm varchar(24) NOT NULL DEFAULT 'ed25519' CHECK (signature_algorithm='ed25519'),
|
||||
signature varchar(500) NOT NULL,
|
||||
signing_key_id varchar(120) NOT NULL,
|
||||
minimum_security_version varchar(80),
|
||||
release_notes varchar(2000),
|
||||
status varchar(24) NOT NULL DEFAULT 'UPLOADING'
|
||||
CHECK (status IN ('UPLOADING','READY','PAUSED','RETIRED')),
|
||||
created_by uuid NOT NULL,
|
||||
verified_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT firmware_release_creator_tenant_fk FOREIGN KEY(created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT firmware_release_household_tenant_fk FOREIGN KEY(household_id,tenant_id) REFERENCES household(id,tenant_id),
|
||||
CONSTRAINT firmware_release_signing_key_fk FOREIGN KEY(tenant_id,signing_key_id) REFERENCES firmware_signing_key(tenant_id,key_id),
|
||||
CONSTRAINT firmware_release_version_key UNIQUE(tenant_id,model,hardware_revision,version)
|
||||
);
|
||||
CREATE INDEX firmware_release_tenant_model_idx ON firmware_release(tenant_id,model,status,created_at DESC);
|
||||
|
||||
CREATE TABLE firmware_rollout (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
release_id uuid NOT NULL REFERENCES firmware_release(id),
|
||||
name varchar(160) NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'DRAFT'
|
||||
CHECK (status IN ('DRAFT','RUNNING','PAUSED','COMPLETED','ROLLED_BACK')),
|
||||
target_percentage integer NOT NULL CHECK (target_percentage BETWEEN 1 AND 100),
|
||||
created_by uuid NOT NULL,
|
||||
started_at timestamptz,
|
||||
completed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT firmware_rollout_household_tenant_fk FOREIGN KEY(household_id,tenant_id) REFERENCES household(id,tenant_id),
|
||||
CONSTRAINT firmware_rollout_creator_tenant_fk FOREIGN KEY(created_by,tenant_id) REFERENCES user_account(id,tenant_id)
|
||||
);
|
||||
|
||||
CREATE TABLE firmware_assignment (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
rollout_id uuid NOT NULL REFERENCES firmware_rollout(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
command_id uuid REFERENCES device_command(id),
|
||||
previous_version varchar(80),
|
||||
status varchar(24) NOT NULL DEFAULT 'PENDING'
|
||||
CHECK (status IN ('PENDING','COMMAND_QUEUED','SUCCEEDED','FAILED','ROLLED_BACK')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT firmware_assignment_device_tenant_fk FOREIGN KEY(device_id,tenant_id) REFERENCES device(id,tenant_id),
|
||||
CONSTRAINT firmware_assignment_rollout_device_key UNIQUE(rollout_id,device_id)
|
||||
);
|
||||
CREATE INDEX firmware_assignment_device_idx ON firmware_assignment(device_id,status,created_at DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,18 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE firmware_assignment DROP CONSTRAINT firmware_assignment_status_check;
|
||||
UPDATE firmware_assignment SET status=CASE status
|
||||
WHEN 'ASSIGNED' THEN 'COMMAND_QUEUED'
|
||||
WHEN 'DOWNLOADING' THEN 'COMMAND_QUEUED'
|
||||
WHEN 'INSTALLING' THEN 'COMMAND_QUEUED'
|
||||
WHEN 'CONFIRMED' THEN 'SUCCEEDED'
|
||||
ELSE status
|
||||
END;
|
||||
ALTER TABLE firmware_assignment
|
||||
DROP COLUMN progress_percent,
|
||||
DROP COLUMN last_result,
|
||||
DROP COLUMN last_reported_at,
|
||||
ADD CONSTRAINT firmware_assignment_status_check
|
||||
CHECK(status IN ('PENDING','COMMAND_QUEUED','SUCCEEDED','FAILED','ROLLED_BACK'));
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,17 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE firmware_assignment DROP CONSTRAINT firmware_assignment_status_check;
|
||||
UPDATE firmware_assignment SET status=CASE status
|
||||
WHEN 'PENDING' THEN 'ASSIGNED'
|
||||
WHEN 'COMMAND_QUEUED' THEN 'ASSIGNED'
|
||||
WHEN 'SUCCEEDED' THEN 'CONFIRMED'
|
||||
ELSE status
|
||||
END;
|
||||
ALTER TABLE firmware_assignment
|
||||
ADD COLUMN progress_percent integer CHECK(progress_percent BETWEEN 0 AND 100),
|
||||
ADD COLUMN last_result jsonb NOT NULL DEFAULT '{}'::jsonb CHECK(jsonb_typeof(last_result)='object'),
|
||||
ADD COLUMN last_reported_at timestamptz,
|
||||
ADD CONSTRAINT firmware_assignment_status_check
|
||||
CHECK(status IN ('ASSIGNED','DOWNLOADING','INSTALLING','CONFIRMED','FAILED','ROLLED_BACK'));
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS media_access_log;
|
||||
DROP TABLE IF EXISTS recording_access_grant;
|
||||
DROP TABLE IF EXISTS upload_batch_segment;
|
||||
DROP TABLE IF EXISTS media_segment;
|
||||
DROP TABLE IF EXISTS upload_batch;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,134 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE upload_batch (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
conversation_id uuid NOT NULL,
|
||||
device_id uuid NOT NULL,
|
||||
idempotency_key varchar(128) NOT NULL,
|
||||
manifest_digest char(64) NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'PENDING'
|
||||
CHECK (status IN ('PENDING', 'UPLOADING', 'VERIFYING', 'COMPLETED', 'FAILED', 'EXPIRED')),
|
||||
segment_count integer NOT NULL CHECK (segment_count > 0),
|
||||
expected_total_size bigint NOT NULL CHECK (expected_total_size > 0),
|
||||
verified_count integer NOT NULL DEFAULT 0 CHECK (verified_count >= 0),
|
||||
rejected_count integer NOT NULL DEFAULT 0 CHECK (rejected_count >= 0),
|
||||
verification_attempts integer NOT NULL DEFAULT 0 CHECK (verification_attempts >= 0),
|
||||
last_error_code varchar(80),
|
||||
expires_at timestamptz NOT NULL,
|
||||
completed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT upload_batch_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT upload_batch_device_tenant_fk FOREIGN KEY (device_id, tenant_id)
|
||||
REFERENCES device (id, tenant_id),
|
||||
CONSTRAINT upload_batch_id_tenant_key UNIQUE (id, tenant_id),
|
||||
CONSTRAINT upload_batch_idempotency_key UNIQUE (device_id, conversation_id, idempotency_key)
|
||||
);
|
||||
CREATE INDEX upload_batch_device_status_idx ON upload_batch (device_id, status, updated_at DESC);
|
||||
CREATE INDEX upload_batch_conversation_time_idx ON upload_batch (tenant_id, conversation_id, created_at DESC);
|
||||
|
||||
CREATE TABLE media_segment (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
conversation_id uuid NOT NULL,
|
||||
device_id uuid NOT NULL,
|
||||
track varchar(8) NOT NULL CHECK (track IN ('near', 'far')),
|
||||
sequence_no integer NOT NULL CHECK (sequence_no >= 0),
|
||||
codec varchar(32) NOT NULL,
|
||||
content_type varchar(100) NOT NULL,
|
||||
duration_ms integer NOT NULL CHECK (duration_ms > 0),
|
||||
expected_size bigint NOT NULL CHECK (expected_size > 0),
|
||||
expected_digest char(64) NOT NULL,
|
||||
object_ref varchar(700) UNIQUE,
|
||||
encryption_key_ref varchar(300) NOT NULL DEFAULT 'object-store-managed',
|
||||
status varchar(24) NOT NULL DEFAULT 'DECLARED'
|
||||
CHECK (status IN ('DECLARED', 'AUTHORIZED', 'UPLOADED', 'VERIFYING', 'VERIFIED', 'REJECTED', 'DELETED')),
|
||||
verification_attempts integer NOT NULL DEFAULT 0 CHECK (verification_attempts >= 0),
|
||||
last_error_code varchar(80),
|
||||
uploaded_at timestamptz,
|
||||
verified_at timestamptz,
|
||||
deleted_at timestamptz,
|
||||
local_delete_after timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT media_segment_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT media_segment_device_tenant_fk FOREIGN KEY (device_id, tenant_id)
|
||||
REFERENCES device (id, tenant_id),
|
||||
CONSTRAINT media_segment_id_tenant_key UNIQUE (id, tenant_id),
|
||||
CONSTRAINT media_segment_track_sequence_key UNIQUE (conversation_id, track, sequence_no)
|
||||
);
|
||||
CREATE INDEX media_segment_conversation_track_idx ON media_segment (tenant_id, conversation_id, track, sequence_no);
|
||||
|
||||
CREATE TABLE upload_batch_segment (
|
||||
upload_batch_id uuid NOT NULL,
|
||||
media_segment_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (upload_batch_id, media_segment_id),
|
||||
CONSTRAINT upload_batch_segment_batch_tenant_fk FOREIGN KEY (upload_batch_id, tenant_id)
|
||||
REFERENCES upload_batch (id, tenant_id),
|
||||
CONSTRAINT upload_batch_segment_media_tenant_fk FOREIGN KEY (media_segment_id, tenant_id)
|
||||
REFERENCES media_segment (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX upload_batch_segment_media_idx ON upload_batch_segment (media_segment_id, upload_batch_id);
|
||||
|
||||
CREATE TABLE recording_access_grant (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
subject_id uuid NOT NULL,
|
||||
grantee_user_id uuid NOT NULL,
|
||||
permission varchar(16) NOT NULL CHECK (permission IN ('PLAY', 'DOWNLOAD')),
|
||||
purpose varchar(120) NOT NULL,
|
||||
granted_by uuid NOT NULL,
|
||||
granted_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz,
|
||||
revoked_by uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT recording_access_subject_tenant_fk FOREIGN KEY (subject_id, tenant_id)
|
||||
REFERENCES subject_profile (id, tenant_id),
|
||||
CONSTRAINT recording_access_grantee_tenant_fk FOREIGN KEY (grantee_user_id, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT recording_access_grantor_tenant_fk FOREIGN KEY (granted_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT recording_access_revoker_tenant_fk FOREIGN KEY (revoked_by, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT recording_access_expiry_check CHECK (expires_at > granted_at),
|
||||
CONSTRAINT recording_access_revoke_check CHECK (revoked_at IS NULL OR revoked_at >= granted_at),
|
||||
CONSTRAINT recording_access_id_tenant_key UNIQUE (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX recording_access_lookup_idx
|
||||
ON recording_access_grant (tenant_id, subject_id, grantee_user_id, permission, expires_at DESC)
|
||||
WHERE revoked_at IS NULL;
|
||||
|
||||
CREATE TABLE media_access_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
actor_user_id uuid NOT NULL,
|
||||
conversation_id uuid NOT NULL,
|
||||
media_segment_id uuid NOT NULL,
|
||||
action varchar(16) NOT NULL CHECK (action IN ('PLAY', 'DOWNLOAD')),
|
||||
purpose varchar(120) NOT NULL,
|
||||
result varchar(16) NOT NULL CHECK (result IN ('SUCCESS', 'DENIED', 'FAILED')),
|
||||
reason_code varchar(80),
|
||||
request_id varchar(128),
|
||||
occurred_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT media_access_actor_tenant_fk FOREIGN KEY (actor_user_id, tenant_id)
|
||||
REFERENCES user_account (id, tenant_id),
|
||||
CONSTRAINT media_access_conversation_tenant_fk FOREIGN KEY (conversation_id, tenant_id)
|
||||
REFERENCES conversation (id, tenant_id),
|
||||
CONSTRAINT media_access_segment_tenant_fk FOREIGN KEY (media_segment_id, tenant_id)
|
||||
REFERENCES media_segment (id, tenant_id)
|
||||
);
|
||||
CREATE INDEX media_access_resource_time_idx ON media_access_log (tenant_id, media_segment_id, occurred_at DESC);
|
||||
CREATE INDEX media_access_actor_time_idx ON media_access_log (tenant_id, actor_user_id, occurred_at DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,10 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS media_segment_local_delete_pending_idx;
|
||||
|
||||
ALTER TABLE media_segment
|
||||
DROP COLUMN IF EXISTS local_delete_error_code,
|
||||
DROP COLUMN IF EXISTS local_delete_reported_at,
|
||||
DROP COLUMN IF EXISTS local_delete_status;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,13 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE media_segment
|
||||
ADD COLUMN local_delete_status varchar(16)
|
||||
CHECK (local_delete_status IN ('DELETED', 'FAILED')),
|
||||
ADD COLUMN local_delete_reported_at timestamptz,
|
||||
ADD COLUMN local_delete_error_code varchar(80);
|
||||
|
||||
CREATE INDEX media_segment_local_delete_pending_idx
|
||||
ON media_segment (device_id, local_delete_after)
|
||||
WHERE status = 'VERIFIED' AND local_delete_status IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,21 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TRIGGER IF EXISTS device_command_web_event ON device_command;
|
||||
DROP TRIGGER IF EXISTS privacy_web_event ON privacy_request;
|
||||
DROP TRIGGER IF EXISTS export_web_event ON export_task;
|
||||
DROP TRIGGER IF EXISTS media_batch_web_event ON upload_batch;
|
||||
DROP TRIGGER IF EXISTS story_web_event ON story_draft;
|
||||
DROP TRIGGER IF EXISTS conversation_web_event ON conversation;
|
||||
DROP FUNCTION IF EXISTS device_command_web_event_trigger();
|
||||
DROP FUNCTION IF EXISTS privacy_web_event_trigger();
|
||||
DROP FUNCTION IF EXISTS export_web_event_trigger();
|
||||
DROP FUNCTION IF EXISTS media_batch_web_event_trigger();
|
||||
DROP FUNCTION IF EXISTS story_web_event_trigger();
|
||||
DROP FUNCTION IF EXISTS conversation_web_event_trigger();
|
||||
DROP FUNCTION IF EXISTS expire_device_presence(timestamptz,integer);
|
||||
DROP FUNCTION IF EXISTS record_device_presence(uuid,uuid,timestamptz);
|
||||
DROP TABLE IF EXISTS device_presence_state;
|
||||
DROP FUNCTION IF EXISTS append_web_event(uuid,uuid,uuid,varchar,varchar,uuid,varchar);
|
||||
DROP TABLE IF EXISTS web_event;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,175 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE web_event (
|
||||
id bigserial PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
subject_id uuid,
|
||||
event_type varchar(80) NOT NULL CHECK (event_type IN (
|
||||
'device.presence.changed',
|
||||
'conversation.started','conversation.completed','conversation.failed',
|
||||
'story.created','story.revised','media.upload.completed','export.ready',
|
||||
'privacy_request.updated','device.command.updated')),
|
||||
resource_type varchar(40) NOT NULL,
|
||||
resource_public_id uuid NOT NULL,
|
||||
status varchar(40) NOT NULL,
|
||||
occurred_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL DEFAULT (now() + interval '15 minutes'),
|
||||
CONSTRAINT web_event_household_tenant_fk FOREIGN KEY (household_id,tenant_id) REFERENCES household(id,tenant_id),
|
||||
CONSTRAINT web_event_subject_tenant_fk FOREIGN KEY (subject_id,tenant_id) REFERENCES subject_profile(id,tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX web_event_household_cursor_idx ON web_event (tenant_id,household_id,id);
|
||||
CREATE INDEX web_event_expiry_idx ON web_event (expires_at);
|
||||
|
||||
CREATE FUNCTION append_web_event(
|
||||
p_tenant uuid,p_household uuid,p_subject uuid,p_event_type varchar,
|
||||
p_resource_type varchar,p_resource_public_id uuid,p_status varchar
|
||||
) RETURNS void LANGUAGE plpgsql AS $$
|
||||
DECLARE new_id bigint;
|
||||
BEGIN
|
||||
INSERT INTO web_event(tenant_id,household_id,subject_id,event_type,resource_type,resource_public_id,status)
|
||||
VALUES(p_tenant,p_household,p_subject,p_event_type,p_resource_type,p_resource_public_id,p_status)
|
||||
RETURNING id INTO new_id;
|
||||
PERFORM pg_notify('web_event',new_id::text);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE device_presence_state (
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
household_id uuid NOT NULL,
|
||||
subject_id uuid NOT NULL,
|
||||
online boolean NOT NULL,
|
||||
last_seen_at timestamptz NOT NULL,
|
||||
changed_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (tenant_id,device_id),
|
||||
CONSTRAINT device_presence_device_tenant_fk FOREIGN KEY (device_id,tenant_id) REFERENCES device(id,tenant_id),
|
||||
CONSTRAINT device_presence_household_tenant_fk FOREIGN KEY (household_id,tenant_id) REFERENCES household(id,tenant_id),
|
||||
CONSTRAINT device_presence_subject_tenant_fk FOREIGN KEY (subject_id,tenant_id) REFERENCES subject_profile(id,tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX device_presence_online_seen_idx ON device_presence_state (online,last_seen_at) WHERE online;
|
||||
|
||||
CREATE FUNCTION record_device_presence(
|
||||
p_tenant uuid,p_device uuid,p_observed_at timestamptz
|
||||
) RETURNS void LANGUAGE plpgsql AS $$
|
||||
DECLARE binding device_binding%ROWTYPE; device_public uuid; was_online boolean; had_state boolean;
|
||||
BEGIN
|
||||
SELECT * INTO binding FROM device_binding
|
||||
WHERE tenant_id=p_tenant AND device_id=p_device AND unbound_at IS NULL;
|
||||
IF NOT FOUND THEN RETURN; END IF;
|
||||
SELECT public_id INTO device_public FROM device WHERE id=p_device AND tenant_id=p_tenant;
|
||||
SELECT online INTO was_online FROM device_presence_state
|
||||
WHERE tenant_id=p_tenant AND device_id=p_device FOR UPDATE;
|
||||
had_state := FOUND;
|
||||
INSERT INTO device_presence_state(tenant_id,device_id,household_id,subject_id,online,last_seen_at,changed_at)
|
||||
VALUES(p_tenant,p_device,binding.household_id,binding.subject_id,true,p_observed_at,p_observed_at)
|
||||
ON CONFLICT (tenant_id,device_id) DO UPDATE SET
|
||||
household_id=EXCLUDED.household_id,subject_id=EXCLUDED.subject_id,online=true,
|
||||
last_seen_at=GREATEST(device_presence_state.last_seen_at,EXCLUDED.last_seen_at),
|
||||
changed_at=CASE WHEN device_presence_state.online THEN device_presence_state.changed_at ELSE EXCLUDED.changed_at END;
|
||||
IF NOT had_state OR NOT was_online THEN
|
||||
PERFORM append_web_event(p_tenant,binding.household_id,binding.subject_id,
|
||||
'device.presence.changed','device',device_public,'online');
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION expire_device_presence(
|
||||
p_stale_before timestamptz,p_limit integer
|
||||
) RETURNS integer LANGUAGE plpgsql AS $$
|
||||
DECLARE item record; expired_count integer := 0;
|
||||
BEGIN
|
||||
FOR item IN
|
||||
SELECT state.tenant_id,state.device_id,state.household_id,state.subject_id,device.public_id
|
||||
FROM device_presence_state state
|
||||
JOIN device ON device.id=state.device_id AND device.tenant_id=state.tenant_id
|
||||
WHERE state.online AND state.last_seen_at<p_stale_before
|
||||
ORDER BY state.last_seen_at
|
||||
FOR UPDATE OF state SKIP LOCKED LIMIT p_limit
|
||||
LOOP
|
||||
UPDATE device_presence_state SET online=false,changed_at=now()
|
||||
WHERE tenant_id=item.tenant_id AND device_id=item.device_id AND online;
|
||||
IF FOUND THEN
|
||||
PERFORM append_web_event(item.tenant_id,item.household_id,item.subject_id,
|
||||
'device.presence.changed','device',item.public_id,'offline');
|
||||
expired_count := expired_count + 1;
|
||||
END IF;
|
||||
END LOOP;
|
||||
RETURN expired_count;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION conversation_web_event_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE mapped varchar;
|
||||
BEGIN
|
||||
IF TG_OP='UPDATE' AND NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;
|
||||
mapped := CASE NEW.status WHEN 'ACTIVE' THEN 'conversation.started' WHEN 'COMPLETED' THEN 'conversation.completed' WHEN 'FAILED' THEN 'conversation.failed' END;
|
||||
IF mapped IS NOT NULL THEN
|
||||
PERFORM append_web_event(NEW.tenant_id,NEW.household_id,NEW.subject_id,mapped,'conversation',NEW.public_id,NEW.status);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER conversation_web_event AFTER INSERT OR UPDATE OF status ON conversation FOR EACH ROW EXECUTE FUNCTION conversation_web_event_trigger();
|
||||
|
||||
CREATE FUNCTION story_web_event_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE household uuid; mapped varchar;
|
||||
BEGIN
|
||||
IF TG_OP='UPDATE' AND NEW.version IS NOT DISTINCT FROM OLD.version THEN RETURN NEW; END IF;
|
||||
SELECT household_id INTO household FROM subject_profile WHERE id=NEW.subject_id AND tenant_id=NEW.tenant_id;
|
||||
mapped := CASE WHEN TG_OP='INSERT' THEN 'story.created' ELSE 'story.revised' END;
|
||||
PERFORM append_web_event(NEW.tenant_id,household,NEW.subject_id,mapped,'story',NEW.public_id,NEW.status);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER story_web_event AFTER INSERT OR UPDATE OF version ON story_draft FOR EACH ROW EXECUTE FUNCTION story_web_event_trigger();
|
||||
|
||||
CREATE FUNCTION media_batch_web_event_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE c conversation%ROWTYPE;
|
||||
BEGIN
|
||||
IF NEW.status='COMPLETED' AND (TG_OP='INSERT' OR OLD.status IS DISTINCT FROM NEW.status) THEN
|
||||
SELECT * INTO c FROM conversation WHERE id=NEW.conversation_id AND tenant_id=NEW.tenant_id;
|
||||
PERFORM append_web_event(NEW.tenant_id,c.household_id,c.subject_id,'media.upload.completed','upload_batch',NEW.public_id,NEW.status);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER media_batch_web_event AFTER INSERT OR UPDATE OF status ON upload_batch FOR EACH ROW EXECUTE FUNCTION media_batch_web_event_trigger();
|
||||
|
||||
CREATE FUNCTION export_web_event_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE household uuid;
|
||||
BEGIN
|
||||
IF NEW.status='READY' AND (TG_OP='INSERT' OR OLD.status IS DISTINCT FROM NEW.status) THEN
|
||||
SELECT household_id INTO household FROM subject_profile WHERE id=NEW.subject_id AND tenant_id=NEW.tenant_id;
|
||||
PERFORM append_web_event(NEW.tenant_id,household,NEW.subject_id,'export.ready','export_task',NEW.public_id,NEW.status);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER export_web_event AFTER INSERT OR UPDATE OF status ON export_task FOR EACH ROW EXECUTE FUNCTION export_web_event_trigger();
|
||||
|
||||
CREATE FUNCTION privacy_web_event_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE household uuid;
|
||||
BEGIN
|
||||
IF TG_OP='UPDATE' AND NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;
|
||||
SELECT household_id INTO household FROM subject_profile WHERE id=NEW.subject_id AND tenant_id=NEW.tenant_id;
|
||||
PERFORM append_web_event(NEW.tenant_id,household,NEW.subject_id,'privacy_request.updated','privacy_request',NEW.public_id,NEW.status);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER privacy_web_event AFTER INSERT OR UPDATE OF status ON privacy_request FOR EACH ROW EXECUTE FUNCTION privacy_web_event_trigger();
|
||||
|
||||
CREATE FUNCTION device_command_web_event_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE binding device_binding%ROWTYPE;
|
||||
BEGIN
|
||||
IF TG_OP='UPDATE' AND NEW.status IS NOT DISTINCT FROM OLD.status THEN RETURN NEW; END IF;
|
||||
SELECT * INTO binding FROM device_binding WHERE device_id=NEW.device_id AND tenant_id=NEW.tenant_id AND unbound_at IS NULL;
|
||||
IF FOUND THEN
|
||||
PERFORM append_web_event(NEW.tenant_id,binding.household_id,binding.subject_id,'device.command.updated','device_command',NEW.public_id,NEW.status);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER device_command_web_event AFTER INSERT OR UPDATE OF status ON device_command FOR EACH ROW EXECUTE FUNCTION device_command_web_event_trigger();
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,4 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS sharing_access_log;
|
||||
DROP TABLE IF EXISTS sharing_grant;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,56 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE sharing_grant (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
household_id uuid NOT NULL,
|
||||
subject_id uuid NOT NULL,
|
||||
resource_type varchar(32) NOT NULL CHECK (resource_type IN ('STORY','TRANSCRIPT','RECORDING')),
|
||||
resource_public_id uuid NOT NULL,
|
||||
grantee_user_id uuid NOT NULL,
|
||||
permission varchar(24) NOT NULL CHECK (permission IN ('VIEW_TEXT','LISTEN','DOWNLOAD','EDIT')),
|
||||
purpose varchar(500) NOT NULL CHECK (length(trim(purpose))>0),
|
||||
login_required boolean NOT NULL DEFAULT true,
|
||||
granted_by uuid NOT NULL,
|
||||
granted_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz,
|
||||
revoked_by uuid,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version>0),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT sharing_grant_household_tenant_fk FOREIGN KEY (household_id,tenant_id) REFERENCES household(id,tenant_id),
|
||||
CONSTRAINT sharing_grant_subject_tenant_fk FOREIGN KEY (subject_id,tenant_id) REFERENCES subject_profile(id,tenant_id) ON DELETE CASCADE,
|
||||
CONSTRAINT sharing_grant_grantee_tenant_fk FOREIGN KEY (grantee_user_id,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT sharing_grant_grantor_tenant_fk FOREIGN KEY (granted_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT sharing_grant_revoker_tenant_fk FOREIGN KEY (revoked_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT sharing_grant_permission_resource_check CHECK (
|
||||
(resource_type='STORY' AND permission IN ('VIEW_TEXT','EDIT')) OR
|
||||
(resource_type='TRANSCRIPT' AND permission='VIEW_TEXT') OR
|
||||
(resource_type='RECORDING' AND permission IN ('LISTEN','DOWNLOAD'))
|
||||
),
|
||||
CONSTRAINT sharing_grant_expiry_check CHECK (expires_at>granted_at)
|
||||
);
|
||||
CREATE INDEX sharing_grant_subject_idx ON sharing_grant(tenant_id,subject_id,granted_at DESC);
|
||||
CREATE INDEX sharing_grant_recipient_idx ON sharing_grant(tenant_id,grantee_user_id,expires_at DESC) WHERE revoked_at IS NULL;
|
||||
CREATE INDEX sharing_grant_resource_idx ON sharing_grant(tenant_id,resource_type,resource_public_id);
|
||||
|
||||
CREATE TABLE sharing_access_log (
|
||||
id bigserial PRIMARY KEY,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
sharing_grant_id uuid NOT NULL REFERENCES sharing_grant(id) ON DELETE CASCADE,
|
||||
actor_user_id uuid,
|
||||
actor_identifier_digest char(64) NOT NULL,
|
||||
resource_type varchar(32) NOT NULL,
|
||||
resource_public_id uuid NOT NULL,
|
||||
permission varchar(24) NOT NULL,
|
||||
result varchar(16) NOT NULL CHECK (result IN ('SUCCESS','DENIED')),
|
||||
reason_code varchar(80),
|
||||
request_id varchar(128),
|
||||
accessed_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT sharing_access_actor_tenant_fk FOREIGN KEY (actor_user_id,tenant_id) REFERENCES user_account(id,tenant_id)
|
||||
);
|
||||
CREATE INDEX sharing_access_grant_time_idx ON sharing_access_log(sharing_grant_id,accessed_at DESC);
|
||||
CREATE INDEX sharing_access_tenant_time_idx ON sharing_access_log(tenant_id,accessed_at DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
DROP INDEX IF EXISTS sharing_grant_token_digest_key;
|
||||
ALTER TABLE sharing_grant DROP CONSTRAINT IF EXISTS sharing_grant_edit_login_check;
|
||||
ALTER TABLE sharing_grant DROP CONSTRAINT IF EXISTS sharing_grant_token_mode_check;
|
||||
ALTER TABLE sharing_grant DROP COLUMN IF EXISTS access_token_digest;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
ALTER TABLE sharing_grant ADD COLUMN access_token_digest char(64);
|
||||
ALTER TABLE sharing_grant ADD CONSTRAINT sharing_grant_token_mode_check CHECK (login_required OR access_token_digest IS NOT NULL);
|
||||
ALTER TABLE sharing_grant ADD CONSTRAINT sharing_grant_edit_login_check CHECK (permission<>'EDIT' OR login_required);
|
||||
CREATE UNIQUE INDEX sharing_grant_token_digest_key ON sharing_grant(access_token_digest) WHERE access_token_digest IS NOT NULL;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS runtime_config_version;
|
||||
ALTER TABLE user_account DROP COLUMN IF EXISTS platform_role;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,53 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE user_account
|
||||
ADD COLUMN platform_role varchar(24) NOT NULL DEFAULT 'customer'
|
||||
CHECK (platform_role IN ('customer','ops_viewer','ops_operator','ops_approver','ops_admin'));
|
||||
|
||||
CREATE TABLE runtime_config_version (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
config_key varchar(100) NOT NULL CHECK (config_key ~ '^[a-z][a-z0-9_.-]{2,99}$'),
|
||||
display_name varchar(200) NOT NULL CHECK (length(trim(display_name))>0),
|
||||
config_kind varchar(24) NOT NULL CHECK (config_kind IN ('PERSONA','VOICE','MODEL','ROUTING','AGENT_BUNDLE')),
|
||||
version_no integer NOT NULL CHECK (version_no>0),
|
||||
status varchar(24) NOT NULL DEFAULT 'DRAFT'
|
||||
CHECK (status IN ('DRAFT','PENDING_APPROVAL','PUBLISHED','RETIRED')),
|
||||
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
|
||||
secret_refs jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(secret_refs)='object'),
|
||||
change_summary varchar(500) NOT NULL CHECK (length(trim(change_summary))>0),
|
||||
validation_result jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(validation_result)='object'),
|
||||
created_by uuid NOT NULL,
|
||||
submitted_by uuid,
|
||||
submitted_at timestamptz,
|
||||
approved_by uuid,
|
||||
approved_at timestamptz,
|
||||
published_by uuid,
|
||||
published_at timestamptz,
|
||||
retired_at timestamptz,
|
||||
supersedes_id uuid REFERENCES runtime_config_version(id),
|
||||
rollback_of_id uuid REFERENCES runtime_config_version(id),
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT runtime_config_creator_tenant_fk FOREIGN KEY (created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT runtime_config_submitter_tenant_fk FOREIGN KEY (submitted_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT runtime_config_approver_tenant_fk FOREIGN KEY (approved_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT runtime_config_publisher_tenant_fk FOREIGN KEY (published_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT runtime_config_version_key UNIQUE (tenant_id,config_key,version_no),
|
||||
CONSTRAINT runtime_config_submission_check CHECK (
|
||||
(status='DRAFT' AND submitted_at IS NULL AND approved_at IS NULL AND published_at IS NULL) OR
|
||||
(status='PENDING_APPROVAL' AND submitted_by IS NOT NULL AND submitted_at IS NOT NULL AND approved_at IS NULL AND published_at IS NULL) OR
|
||||
(status='PUBLISHED' AND submitted_by IS NOT NULL AND submitted_at IS NOT NULL AND approved_by IS NOT NULL AND approved_at IS NOT NULL AND published_by IS NOT NULL AND published_at IS NOT NULL) OR
|
||||
(status='RETIRED' AND retired_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE UNIQUE INDEX runtime_config_one_published_key
|
||||
ON runtime_config_version(tenant_id,config_key) WHERE status='PUBLISHED';
|
||||
CREATE INDEX runtime_config_list_idx
|
||||
ON runtime_config_version(tenant_id,config_key,version_no DESC);
|
||||
CREATE INDEX runtime_config_status_idx
|
||||
ON runtime_config_version(tenant_id,status,updated_at DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,20 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS runtime_config_rollout;
|
||||
DROP TABLE IF EXISTS ops_device_group_member;
|
||||
DROP TABLE IF EXISTS ops_device_group;
|
||||
DROP TABLE IF EXISTS ops_config_evaluation;
|
||||
DROP TABLE IF EXISTS ops_resource_reference;
|
||||
|
||||
WITH ranked AS (
|
||||
SELECT id,row_number() OVER (PARTITION BY tenant_id,config_key ORDER BY version_no DESC,id DESC) AS position
|
||||
FROM runtime_config_version WHERE status='PUBLISHED'
|
||||
)
|
||||
UPDATE runtime_config_version rcv
|
||||
SET status='RETIRED',retired_at=COALESCE(retired_at,now()),row_version=row_version+1,updated_at=now()
|
||||
FROM ranked WHERE ranked.id=rcv.id AND ranked.position>1;
|
||||
|
||||
CREATE UNIQUE INDEX runtime_config_one_published_key
|
||||
ON runtime_config_version(tenant_id,config_key) WHERE status='PUBLISHED';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,101 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX runtime_config_one_published_key;
|
||||
|
||||
CREATE TABLE ops_resource_reference (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
resource_type varchar(32) NOT NULL CHECK (resource_type IN ('PROVIDER_CREDENTIAL','VOICE_RESOURCE','MODEL_RESOURCE')),
|
||||
reference_name varchar(200) NOT NULL CHECK (reference_name ~ '^[A-Za-z][A-Za-z0-9_.:/-]{2,199}$'),
|
||||
display_name varchar(200) NOT NULL CHECK (length(trim(display_name))>0),
|
||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','REVOKED')),
|
||||
verified_at timestamptz NOT NULL,
|
||||
last_rotated_at timestamptz,
|
||||
created_by uuid NOT NULL,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ops_resource_creator_tenant_fk FOREIGN KEY (created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_resource_reference_key UNIQUE (tenant_id,reference_name)
|
||||
);
|
||||
CREATE INDEX ops_resource_reference_status_idx ON ops_resource_reference(tenant_id,status,resource_type);
|
||||
|
||||
CREATE TABLE ops_config_evaluation (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
config_version_id uuid NOT NULL REFERENCES runtime_config_version(id) ON DELETE CASCADE,
|
||||
suite_version varchar(100) NOT NULL CHECK (length(trim(suite_version))>0),
|
||||
result varchar(16) NOT NULL CHECK (result IN ('PASSED','FAILED')),
|
||||
metrics jsonb NOT NULL CHECK (jsonb_typeof(metrics)='object'),
|
||||
report_ref varchar(300) NOT NULL CHECK (length(trim(report_ref))>0),
|
||||
evaluated_by uuid NOT NULL,
|
||||
evaluated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ops_evaluation_actor_tenant_fk FOREIGN KEY (evaluated_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_evaluation_version_suite_key UNIQUE (config_version_id,suite_version)
|
||||
);
|
||||
CREATE INDEX ops_config_evaluation_result_idx ON ops_config_evaluation(tenant_id,config_version_id,result,evaluated_at DESC);
|
||||
|
||||
CREATE TABLE ops_device_group (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
name varchar(200) NOT NULL CHECK (length(trim(name))>0),
|
||||
selector jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(selector)='object'),
|
||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','ARCHIVED')),
|
||||
created_by uuid NOT NULL,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ops_device_group_creator_tenant_fk FOREIGN KEY (created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_device_group_id_tenant_key UNIQUE (id,tenant_id)
|
||||
);
|
||||
CREATE UNIQUE INDEX ops_device_group_active_name_key ON ops_device_group(tenant_id,lower(name)) WHERE status='ACTIVE';
|
||||
|
||||
CREATE TABLE ops_device_group_member (
|
||||
group_id uuid NOT NULL,
|
||||
tenant_id uuid NOT NULL,
|
||||
device_id uuid NOT NULL,
|
||||
added_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (group_id,device_id),
|
||||
CONSTRAINT ops_device_group_member_group_fk FOREIGN KEY (group_id,tenant_id) REFERENCES ops_device_group(id,tenant_id) ON DELETE CASCADE,
|
||||
CONSTRAINT ops_device_group_member_device_fk FOREIGN KEY (device_id,tenant_id) REFERENCES device(id,tenant_id)
|
||||
);
|
||||
CREATE INDEX ops_device_group_member_device_idx ON ops_device_group_member(tenant_id,device_id);
|
||||
|
||||
CREATE TABLE runtime_config_rollout (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
config_version_id uuid NOT NULL REFERENCES runtime_config_version(id),
|
||||
config_key varchar(100) NOT NULL,
|
||||
scope_type varchar(24) NOT NULL CHECK (scope_type IN ('TENANT','SUBJECT','DEVICE_GROUP')),
|
||||
subject_id uuid,
|
||||
device_group_id uuid,
|
||||
rollout_percent integer NOT NULL CHECK (rollout_percent BETWEEN 1 AND 100),
|
||||
max_concurrent_calls integer NOT NULL CHECK (max_concurrent_calls BETWEEN 1 AND 100000),
|
||||
cohort_seed integer NOT NULL DEFAULT 0,
|
||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','PAUSED','ENDED')),
|
||||
reason varchar(500) NOT NULL CHECK (length(trim(reason))>0),
|
||||
activated_by uuid NOT NULL,
|
||||
activated_at timestamptz NOT NULL DEFAULT now(),
|
||||
paused_at timestamptz,
|
||||
ended_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT runtime_rollout_actor_tenant_fk FOREIGN KEY (activated_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT runtime_rollout_subject_tenant_fk FOREIGN KEY (subject_id,tenant_id) REFERENCES subject_profile(id,tenant_id),
|
||||
CONSTRAINT runtime_rollout_group_tenant_fk FOREIGN KEY (device_group_id,tenant_id) REFERENCES ops_device_group(id,tenant_id),
|
||||
CONSTRAINT runtime_rollout_scope_check CHECK (
|
||||
(scope_type='TENANT' AND subject_id IS NULL AND device_group_id IS NULL) OR
|
||||
(scope_type='SUBJECT' AND subject_id IS NOT NULL AND device_group_id IS NULL AND rollout_percent=100) OR
|
||||
(scope_type='DEVICE_GROUP' AND subject_id IS NULL AND device_group_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE UNIQUE INDEX runtime_rollout_active_tenant_key ON runtime_config_rollout(tenant_id,config_key) WHERE status='ACTIVE' AND scope_type='TENANT';
|
||||
CREATE UNIQUE INDEX runtime_rollout_active_subject_key ON runtime_config_rollout(tenant_id,config_key,subject_id) WHERE status='ACTIVE' AND scope_type='SUBJECT';
|
||||
CREATE UNIQUE INDEX runtime_rollout_active_group_key ON runtime_config_rollout(tenant_id,config_key,device_group_id) WHERE status='ACTIVE' AND scope_type='DEVICE_GROUP';
|
||||
CREATE INDEX runtime_rollout_selection_idx ON runtime_config_rollout(tenant_id,config_key,status,scope_type);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,10 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS conversation_usage_budget;
|
||||
DROP TABLE IF EXISTS conversation_cost_line;
|
||||
DROP TABLE IF EXISTS conversation_usage_event;
|
||||
DROP TABLE IF EXISTS usage_quota_policy;
|
||||
DROP TABLE IF EXISTS billing_price_rate;
|
||||
DROP TABLE IF EXISTS billing_price_book;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,129 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE billing_price_book (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
provider varchar(40) NOT NULL CHECK (provider ~ '^[a-z][a-z0-9_.-]{1,39}$'),
|
||||
currency char(3) NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
|
||||
version_no integer NOT NULL CHECK (version_no>0),
|
||||
status varchar(16) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT','PUBLISHED','RETIRED')),
|
||||
source_ref varchar(300) NOT NULL CHECK (length(trim(source_ref))>0),
|
||||
change_summary varchar(500) NOT NULL CHECK (length(trim(change_summary))>0),
|
||||
effective_from timestamptz NOT NULL,
|
||||
created_by uuid NOT NULL,
|
||||
published_by uuid,
|
||||
published_at timestamptz,
|
||||
retired_at timestamptz,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT billing_price_book_creator_tenant_fk FOREIGN KEY (created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT billing_price_book_publisher_tenant_fk FOREIGN KEY (published_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT billing_price_book_version_key UNIQUE (tenant_id,provider,version_no),
|
||||
CONSTRAINT billing_price_book_publish_state CHECK (
|
||||
(status='DRAFT' AND published_by IS NULL AND published_at IS NULL AND retired_at IS NULL) OR
|
||||
(status='PUBLISHED' AND published_by IS NOT NULL AND published_at IS NOT NULL AND retired_at IS NULL) OR
|
||||
(status='RETIRED' AND published_by IS NOT NULL AND published_at IS NOT NULL AND retired_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE UNIQUE INDEX billing_price_book_one_published ON billing_price_book(tenant_id,provider) WHERE status='PUBLISHED';
|
||||
|
||||
CREATE TABLE billing_price_rate (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
price_book_id uuid NOT NULL REFERENCES billing_price_book(id) ON DELETE CASCADE,
|
||||
usage_category varchar(32) NOT NULL CHECK (usage_category IN ('RTC','END_TO_END_VOICE','ASR','LLM','TTS','STORAGE','POST_PROCESSING')),
|
||||
metric_code varchar(80) NOT NULL CHECK (metric_code ~ '^[a-z][a-z0-9_.-]{1,79}$'),
|
||||
unit varchar(40) NOT NULL CHECK (unit ~ '^[a-z][a-z0-9_.-]{1,39}$'),
|
||||
unit_price_micros bigint NOT NULL CHECK (unit_price_micros>=0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT billing_price_rate_key UNIQUE (price_book_id,usage_category,metric_code,unit)
|
||||
);
|
||||
|
||||
CREATE TABLE usage_quota_policy (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
scope_type varchar(16) NOT NULL CHECK (scope_type IN ('TENANT','DEVICE')),
|
||||
device_id uuid,
|
||||
version_no integer NOT NULL CHECK (version_no>0),
|
||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','RETIRED')),
|
||||
billing_timezone varchar(64) NOT NULL CHECK (length(trim(billing_timezone))>0),
|
||||
currency char(3) NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
|
||||
daily_limit_micros bigint NOT NULL CHECK (daily_limit_micros>0),
|
||||
monthly_limit_micros bigint NOT NULL CHECK (monthly_limit_micros>0),
|
||||
max_concurrent_calls integer NOT NULL CHECK (max_concurrent_calls BETWEEN 1 AND 100000),
|
||||
max_call_duration_sec integer NOT NULL CHECK (max_call_duration_sec BETWEEN 60 AND 86400),
|
||||
in_progress_action varchar(32) NOT NULL CHECK (in_progress_action IN ('STOP_IMMEDIATELY','COMPLETE_CURRENT_TURN')),
|
||||
change_summary varchar(500) NOT NULL CHECK (length(trim(change_summary))>0),
|
||||
created_by uuid NOT NULL,
|
||||
retired_at timestamptz,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT usage_quota_policy_creator_tenant_fk FOREIGN KEY (created_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT usage_quota_policy_device_tenant_fk FOREIGN KEY (device_id,tenant_id) REFERENCES device(id,tenant_id),
|
||||
CONSTRAINT usage_quota_policy_scope_check CHECK (
|
||||
(scope_type='TENANT' AND device_id IS NULL) OR (scope_type='DEVICE' AND device_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE UNIQUE INDEX usage_quota_policy_active_tenant ON usage_quota_policy(tenant_id) WHERE status='ACTIVE' AND scope_type='TENANT';
|
||||
CREATE UNIQUE INDEX usage_quota_policy_active_device ON usage_quota_policy(tenant_id,device_id) WHERE status='ACTIVE' AND scope_type='DEVICE';
|
||||
CREATE UNIQUE INDEX usage_quota_policy_tenant_version ON usage_quota_policy(tenant_id,version_no) WHERE scope_type='TENANT';
|
||||
CREATE UNIQUE INDEX usage_quota_policy_device_version ON usage_quota_policy(tenant_id,device_id,version_no) WHERE scope_type='DEVICE';
|
||||
|
||||
CREATE TABLE conversation_usage_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
conversation_id uuid NOT NULL,
|
||||
provider_event_id uuid REFERENCES provider_event(id) ON DELETE SET NULL,
|
||||
provider varchar(40) NOT NULL,
|
||||
source_event_key varchar(255) NOT NULL,
|
||||
usage_category varchar(32) NOT NULL CHECK (usage_category IN ('RTC','END_TO_END_VOICE','ASR','LLM','TTS','STORAGE','POST_PROCESSING')),
|
||||
metric_code varchar(80) NOT NULL CHECK (metric_code ~ '^[a-z][a-z0-9_.-]{1,79}$'),
|
||||
quantity numeric(24,6) NOT NULL CHECK (quantity>=0),
|
||||
unit varchar(40) NOT NULL CHECK (unit ~ '^[a-z][a-z0-9_.-]{1,39}$'),
|
||||
occurred_at timestamptz NOT NULL,
|
||||
received_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT conversation_usage_conversation_tenant_fk FOREIGN KEY (conversation_id,tenant_id) REFERENCES conversation(id,tenant_id) ON DELETE CASCADE,
|
||||
CONSTRAINT conversation_usage_event_key UNIQUE (tenant_id,provider,source_event_key,usage_category,metric_code,unit)
|
||||
);
|
||||
CREATE INDEX conversation_usage_time_idx ON conversation_usage_event(tenant_id,conversation_id,occurred_at,id);
|
||||
|
||||
CREATE TABLE conversation_cost_line (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
conversation_id uuid NOT NULL,
|
||||
usage_event_id uuid NOT NULL UNIQUE REFERENCES conversation_usage_event(id) ON DELETE CASCADE,
|
||||
price_book_id uuid NOT NULL REFERENCES billing_price_book(id),
|
||||
price_rate_id uuid NOT NULL REFERENCES billing_price_rate(id),
|
||||
quantity numeric(24,6) NOT NULL,
|
||||
unit varchar(40) NOT NULL,
|
||||
unit_price_micros bigint NOT NULL CHECK (unit_price_micros>=0),
|
||||
cost_micros bigint NOT NULL CHECK (cost_micros>=0),
|
||||
currency char(3) NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
|
||||
rated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT conversation_cost_conversation_tenant_fk FOREIGN KEY (conversation_id,tenant_id) REFERENCES conversation(id,tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX conversation_cost_tenant_time_idx ON conversation_cost_line(tenant_id,rated_at,conversation_id);
|
||||
|
||||
CREATE TABLE conversation_usage_budget (
|
||||
conversation_id uuid PRIMARY KEY REFERENCES conversation(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
tenant_policy_id uuid REFERENCES usage_quota_policy(id),
|
||||
device_policy_id uuid REFERENCES usage_quota_policy(id),
|
||||
billing_timezone varchar(64) NOT NULL,
|
||||
currency char(3) NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
|
||||
daily_limit_micros bigint NOT NULL CHECK (daily_limit_micros>0),
|
||||
monthly_limit_micros bigint NOT NULL CHECK (monthly_limit_micros>0),
|
||||
max_call_duration_sec integer NOT NULL CHECK (max_call_duration_sec BETWEEN 60 AND 86400),
|
||||
in_progress_action varchar(32) NOT NULL CHECK (in_progress_action IN ('STOP_IMMEDIATELY','COMPLETE_CURRENT_TURN')),
|
||||
daily_spend_at_create bigint NOT NULL CHECK (daily_spend_at_create>=0),
|
||||
monthly_spend_at_create bigint NOT NULL CHECK (monthly_spend_at_create>=0),
|
||||
frozen_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT conversation_usage_budget_tenant_fk FOREIGN KEY (conversation_id,tenant_id) REFERENCES conversation(id,tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,8 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS alert_action;
|
||||
DROP TABLE IF EXISTS alert_incident;
|
||||
DROP TABLE IF EXISTS alert_silence;
|
||||
DROP TABLE IF EXISTS alert_routing_policy;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,95 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE alert_routing_policy (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
category varchar(20) NOT NULL CHECK (category IN ('BUSINESS','TECHNICAL','SECURITY','COST')),
|
||||
severity varchar(16) NOT NULL CHECK (severity IN ('CRITICAL','HIGH','MEDIUM','LOW')),
|
||||
version_no integer NOT NULL CHECK (version_no>0),
|
||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','RETIRED')),
|
||||
receiver_group varchar(120) NOT NULL,
|
||||
response_minutes integer NOT NULL CHECK (response_minutes BETWEEN 1 AND 10080),
|
||||
escalation_after_minutes integer NOT NULL CHECK (escalation_after_minutes BETWEEN 1 AND 10080),
|
||||
escalation_receiver_group varchar(120) NOT NULL,
|
||||
runbook_ref varchar(300) NOT NULL,
|
||||
change_summary varchar(500) NOT NULL,
|
||||
created_by uuid NOT NULL REFERENCES user_account(id),
|
||||
retired_at timestamptz,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT alert_route_version_unique UNIQUE (tenant_id,category,severity,version_no)
|
||||
);
|
||||
CREATE UNIQUE INDEX alert_route_active_unique ON alert_routing_policy(tenant_id,category,severity) WHERE status='ACTIVE';
|
||||
|
||||
CREATE TABLE alert_silence (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
rule_code varchar(100),
|
||||
category varchar(20) CHECK (category IN ('BUSINESS','TECHNICAL','SECURITY','COST')),
|
||||
resource_type varchar(80),
|
||||
resource_public_id uuid,
|
||||
matcher jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
reason varchar(500) NOT NULL,
|
||||
starts_at timestamptz NOT NULL,
|
||||
ends_at timestamptz NOT NULL,
|
||||
created_by uuid NOT NULL REFERENCES user_account(id),
|
||||
revoked_by uuid REFERENCES user_account(id),
|
||||
revoked_at timestamptz,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT alert_silence_scope CHECK (rule_code IS NOT NULL OR category IS NOT NULL OR resource_public_id IS NOT NULL),
|
||||
CONSTRAINT alert_silence_window CHECK (ends_at>starts_at)
|
||||
);
|
||||
CREATE INDEX alert_silence_active_idx ON alert_silence(tenant_id,starts_at,ends_at) WHERE revoked_at IS NULL;
|
||||
|
||||
CREATE TABLE alert_incident (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
rule_code varchar(100) NOT NULL,
|
||||
category varchar(20) NOT NULL CHECK (category IN ('BUSINESS','TECHNICAL','SECURITY','COST')),
|
||||
severity varchar(16) NOT NULL CHECK (severity IN ('CRITICAL','HIGH','MEDIUM','LOW')),
|
||||
status varchar(24) NOT NULL DEFAULT 'OPEN' CHECK (status IN ('OPEN','ACKNOWLEDGED','MITIGATED','RESOLVED','SILENCED')),
|
||||
fingerprint varchar(200) NOT NULL,
|
||||
title varchar(200) NOT NULL,
|
||||
summary varchar(1000) NOT NULL,
|
||||
resource_type varchar(80),
|
||||
resource_public_id uuid,
|
||||
correlation_request_id varchar(128),
|
||||
routing_policy_id uuid REFERENCES alert_routing_policy(id),
|
||||
current_owner_id uuid REFERENCES user_account(id),
|
||||
first_detected_at timestamptz NOT NULL,
|
||||
last_detected_at timestamptz NOT NULL,
|
||||
occurrence_count bigint NOT NULL DEFAULT 1 CHECK (occurrence_count>0),
|
||||
acknowledged_at timestamptz,
|
||||
mitigated_at timestamptz,
|
||||
resolved_at timestamptz,
|
||||
silenced_until timestamptz,
|
||||
escalated_at timestamptz,
|
||||
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version>0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX alert_incident_active_fingerprint ON alert_incident(tenant_id,fingerprint) WHERE status<>'RESOLVED';
|
||||
CREATE INDEX alert_incident_queue_idx ON alert_incident(tenant_id,status,severity,last_detected_at DESC);
|
||||
|
||||
CREATE TABLE alert_action (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
incident_id uuid NOT NULL REFERENCES alert_incident(id) ON DELETE CASCADE,
|
||||
action_type varchar(24) NOT NULL CHECK (action_type IN ('DETECTED','ACKNOWLEDGED','ASSIGNED','SILENCED','MITIGATED','RESOLVED','REOPENED','ESCALATED','COMMENTED')),
|
||||
from_status varchar(24),
|
||||
to_status varchar(24),
|
||||
actor_type varchar(16) NOT NULL CHECK (actor_type IN ('system','user')),
|
||||
actor_user_id uuid REFERENCES user_account(id),
|
||||
note varchar(1000) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX alert_action_incident_time_idx ON alert_action(incident_id,created_at,id);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS provider_dependency_state;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE provider_dependency_state (
|
||||
tenant_id UUID NOT NULL REFERENCES tenant(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE'
|
||||
CHECK (status IN ('AVAILABLE', 'UNAVAILABLE')),
|
||||
consecutive_failures INTEGER NOT NULL DEFAULT 0 CHECK (consecutive_failures >= 0),
|
||||
open_until TIMESTAMPTZ,
|
||||
last_error_code VARCHAR(64),
|
||||
last_failure_at TIMESTAMPTZ,
|
||||
last_success_at TIMESTAMPTZ,
|
||||
row_version BIGINT NOT NULL DEFAULT 1,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (tenant_id, provider)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_provider_dependency_unavailable
|
||||
ON provider_dependency_state (updated_at DESC)
|
||||
WHERE status = 'UNAVAILABLE';
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS device_telemetry_event;
|
||||
|
||||
ALTER TABLE device_twin
|
||||
DROP COLUMN IF EXISTS last_error_at,
|
||||
DROP COLUMN IF EXISTS last_error_severity,
|
||||
DROP COLUMN IF EXISTS last_error_code,
|
||||
DROP COLUMN IF EXISTS telemetry_sequence;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,38 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE device_twin
|
||||
ADD COLUMN telemetry_sequence bigint NOT NULL DEFAULT 0 CHECK (telemetry_sequence >= 0),
|
||||
ADD COLUMN last_error_code varchar(80),
|
||||
ADD COLUMN last_error_severity varchar(16),
|
||||
ADD COLUMN last_error_at timestamptz;
|
||||
|
||||
CREATE TABLE device_telemetry_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
conversation_id uuid,
|
||||
event_key varchar(255) NOT NULL,
|
||||
sequence_no bigint NOT NULL CHECK (sequence_no > 0),
|
||||
event_type varchar(24) NOT NULL CHECK (event_type IN ('status','error','command_result')),
|
||||
occurred_at timestamptz NOT NULL,
|
||||
payload_digest char(64) NOT NULL,
|
||||
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
|
||||
error_code varchar(80),
|
||||
error_severity varchar(16) CHECK (error_severity IS NULL OR error_severity IN ('warning','error','critical')),
|
||||
command_id uuid REFERENCES device_command(id),
|
||||
received_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_telemetry_device_tenant_fk FOREIGN KEY (device_id,tenant_id)
|
||||
REFERENCES device(id,tenant_id),
|
||||
CONSTRAINT device_telemetry_conversation_tenant_fk FOREIGN KEY (conversation_id,tenant_id)
|
||||
REFERENCES conversation(id,tenant_id),
|
||||
CONSTRAINT device_telemetry_event_key UNIQUE (device_id,event_key),
|
||||
CONSTRAINT device_telemetry_sequence_key UNIQUE (device_id,sequence_no)
|
||||
);
|
||||
|
||||
CREATE INDEX device_telemetry_time_idx ON device_telemetry_event(device_id,occurred_at DESC,id);
|
||||
CREATE INDEX device_telemetry_conversation_idx ON device_telemetry_event(conversation_id,occurred_at DESC)
|
||||
WHERE conversation_id IS NOT NULL;
|
||||
CREATE INDEX device_telemetry_error_idx ON device_telemetry_event(tenant_id,error_severity,occurred_at DESC)
|
||||
WHERE event_type='error';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS ops_resource_reference_certificate_expiry_idx;
|
||||
ALTER TABLE ops_resource_reference DROP CONSTRAINT ops_resource_reference_certificate_expiry_check;
|
||||
ALTER TABLE ops_resource_reference DROP COLUMN expires_at;
|
||||
ALTER TABLE ops_resource_reference DROP CONSTRAINT ops_resource_reference_resource_type_check;
|
||||
ALTER TABLE ops_resource_reference
|
||||
ADD CONSTRAINT ops_resource_reference_resource_type_check
|
||||
CHECK (resource_type IN ('PROVIDER_CREDENTIAL','VOICE_RESOURCE','MODEL_RESOURCE'));
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,19 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE ops_resource_reference
|
||||
DROP CONSTRAINT ops_resource_reference_resource_type_check;
|
||||
ALTER TABLE ops_resource_reference
|
||||
ADD CONSTRAINT ops_resource_reference_resource_type_check
|
||||
CHECK (resource_type IN ('PROVIDER_CREDENTIAL','VOICE_RESOURCE','MODEL_RESOURCE','CERTIFICATE'));
|
||||
ALTER TABLE ops_resource_reference
|
||||
ADD COLUMN expires_at timestamptz;
|
||||
ALTER TABLE ops_resource_reference
|
||||
ADD CONSTRAINT ops_resource_reference_certificate_expiry_check CHECK (
|
||||
(resource_type='CERTIFICATE' AND expires_at IS NOT NULL) OR
|
||||
(resource_type<>'CERTIFICATE' AND expires_at IS NULL)
|
||||
);
|
||||
CREATE INDEX ops_resource_reference_certificate_expiry_idx
|
||||
ON ops_resource_reference(tenant_id,expires_at)
|
||||
WHERE resource_type='CERTIFICATE' AND status='ACTIVE';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,12 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS outbox_event_tenant_dead_idx;
|
||||
ALTER TABLE outbox_event
|
||||
DROP CONSTRAINT outbox_event_public_id_key,
|
||||
DROP CONSTRAINT outbox_event_manual_retry_count_check,
|
||||
DROP COLUMN last_manual_retry_by,
|
||||
DROP COLUMN last_manual_retry_at,
|
||||
DROP COLUMN manual_retry_count,
|
||||
DROP COLUMN public_id;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE outbox_event
|
||||
ADD COLUMN public_id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
ADD COLUMN manual_retry_count integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN last_manual_retry_at timestamptz,
|
||||
ADD COLUMN last_manual_retry_by uuid REFERENCES user_account(id),
|
||||
ADD CONSTRAINT outbox_event_manual_retry_count_check CHECK (manual_retry_count >= 0),
|
||||
ADD CONSTRAINT outbox_event_public_id_key UNIQUE (public_id);
|
||||
|
||||
CREATE INDEX outbox_event_tenant_dead_idx
|
||||
ON outbox_event(tenant_id,created_at DESC)
|
||||
WHERE status='dead';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS ops_sensitive_access_log;
|
||||
DROP TABLE IF EXISTS ops_sensitive_access_request;
|
||||
@@ -0,0 +1,62 @@
|
||||
CREATE TABLE ops_sensitive_access_request (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
conversation_id uuid NOT NULL,
|
||||
requester_user_id uuid NOT NULL,
|
||||
capabilities text[] NOT NULL,
|
||||
reason varchar(500) NOT NULL,
|
||||
requested_expires_at timestamptz NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'PENDING'
|
||||
CHECK (status IN ('PENDING','APPROVED','REJECTED','REVOKED','EXPIRED')),
|
||||
reviewed_by uuid,
|
||||
review_reason varchar(500),
|
||||
reviewed_at timestamptz,
|
||||
revoked_by uuid,
|
||||
revoked_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ops_sensitive_access_conversation_tenant_fk FOREIGN KEY (conversation_id,tenant_id) REFERENCES conversation(id,tenant_id),
|
||||
CONSTRAINT ops_sensitive_access_requester_tenant_fk FOREIGN KEY (requester_user_id,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_sensitive_access_reviewer_tenant_fk FOREIGN KEY (reviewed_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_sensitive_access_revoker_tenant_fk FOREIGN KEY (revoked_by,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_sensitive_access_capabilities_check CHECK (
|
||||
capabilities=ARRAY['RECORDING_PLAY']::text[]
|
||||
OR capabilities=ARRAY['TRANSCRIPT_READ']::text[]
|
||||
OR capabilities=ARRAY['RECORDING_PLAY','TRANSCRIPT_READ']::text[]
|
||||
),
|
||||
CONSTRAINT ops_sensitive_access_expiry_check CHECK (requested_expires_at > created_at AND requested_expires_at <= created_at + interval '4 hours'),
|
||||
CONSTRAINT ops_sensitive_access_review_check CHECK (
|
||||
(status='PENDING' AND reviewed_by IS NULL AND reviewed_at IS NULL)
|
||||
OR (status<>'PENDING')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX ops_sensitive_access_one_open_key
|
||||
ON ops_sensitive_access_request(tenant_id,conversation_id,requester_user_id)
|
||||
WHERE status IN ('PENDING','APPROVED');
|
||||
CREATE INDEX ops_sensitive_access_review_queue_idx
|
||||
ON ops_sensitive_access_request(tenant_id,status,created_at DESC);
|
||||
|
||||
CREATE TABLE ops_sensitive_access_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
access_request_id uuid NOT NULL REFERENCES ops_sensitive_access_request(id),
|
||||
actor_user_id uuid NOT NULL,
|
||||
conversation_id uuid NOT NULL,
|
||||
media_segment_id uuid,
|
||||
capability varchar(32) NOT NULL CHECK (capability IN ('TRANSCRIPT_READ','RECORDING_PLAY')),
|
||||
result varchar(16) NOT NULL CHECK (result IN ('SUCCESS','DENIED')),
|
||||
reason_code varchar(64),
|
||||
request_id varchar(128),
|
||||
accessed_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT ops_sensitive_access_log_actor_tenant_fk FOREIGN KEY (actor_user_id,tenant_id) REFERENCES user_account(id,tenant_id),
|
||||
CONSTRAINT ops_sensitive_access_log_conversation_tenant_fk FOREIGN KEY (conversation_id,tenant_id) REFERENCES conversation(id,tenant_id),
|
||||
CONSTRAINT ops_sensitive_access_log_segment_tenant_fk FOREIGN KEY (media_segment_id,tenant_id) REFERENCES media_segment(id,tenant_id)
|
||||
);
|
||||
|
||||
CREATE INDEX ops_sensitive_access_log_request_time_idx
|
||||
ON ops_sensitive_access_log(access_request_id,accessed_at DESC);
|
||||
CREATE INDEX ops_sensitive_access_log_tenant_time_idx
|
||||
ON ops_sensitive_access_log(tenant_id,accessed_at DESC);
|
||||
@@ -0,0 +1,8 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE rtc_task
|
||||
DROP CONSTRAINT IF EXISTS rtc_credential_expiry_after_issue,
|
||||
DROP COLUMN IF EXISTS credential_expires_at,
|
||||
DROP COLUMN IF EXISTS credential_issued_at;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,17 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE rtc_task
|
||||
ADD COLUMN credential_issued_at timestamptz NOT NULL DEFAULT now(),
|
||||
ADD COLUMN credential_expires_at timestamptz NOT NULL DEFAULT (now() + interval '5 minutes');
|
||||
|
||||
UPDATE rtc_task rt
|
||||
SET credential_issued_at = rt.created_at,
|
||||
credential_expires_at = GREATEST(c.allocation_expires_at, rt.created_at + interval '1 second')
|
||||
FROM conversation c
|
||||
WHERE c.id = rt.conversation_id;
|
||||
|
||||
ALTER TABLE rtc_task
|
||||
ADD CONSTRAINT rtc_credential_expiry_after_issue
|
||||
CHECK (credential_expires_at > credential_issued_at);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE device
|
||||
DROP COLUMN minimum_firmware_version;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE device
|
||||
ADD COLUMN minimum_firmware_version varchar(80);
|
||||
|
||||
COMMENT ON COLUMN device.minimum_firmware_version IS
|
||||
'Minimum device firmware accepted for new calls; SemVer is ordered, opaque vendor versions require an exact match.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,3 @@
|
||||
BEGIN;
|
||||
DROP TABLE IF EXISTS integration_issue;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,49 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE integration_issue (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
public_id uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
phenomenon varchar(2000) NOT NULL,
|
||||
device_id uuid,
|
||||
conversation_id uuid,
|
||||
occurred_at timestamptz NOT NULL,
|
||||
request_id varchar(128),
|
||||
responsible_party varchar(120) NOT NULL,
|
||||
status varchar(24) NOT NULL DEFAULT 'OPEN'
|
||||
CHECK (status IN ('OPEN','IN_PROGRESS','RESOLVED','VERIFIED','CLOSED')),
|
||||
resolution_result varchar(4000),
|
||||
regression_conclusion varchar(4000),
|
||||
created_by uuid NOT NULL,
|
||||
updated_by uuid NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT integration_issue_device_tenant_fk
|
||||
FOREIGN KEY (device_id, tenant_id) REFERENCES device(id, tenant_id),
|
||||
CONSTRAINT integration_issue_conversation_tenant_fk
|
||||
FOREIGN KEY (conversation_id, tenant_id) REFERENCES conversation(id, tenant_id),
|
||||
CONSTRAINT integration_issue_creator_tenant_fk
|
||||
FOREIGN KEY (created_by, tenant_id) REFERENCES user_account(id, tenant_id),
|
||||
CONSTRAINT integration_issue_updater_tenant_fk
|
||||
FOREIGN KEY (updated_by, tenant_id) REFERENCES user_account(id, tenant_id),
|
||||
CONSTRAINT integration_issue_resolution_check CHECK (
|
||||
status NOT IN ('RESOLVED','VERIFIED','CLOSED') OR NULLIF(btrim(resolution_result),'') IS NOT NULL
|
||||
),
|
||||
CONSTRAINT integration_issue_regression_check CHECK (
|
||||
status NOT IN ('VERIFIED','CLOSED') OR NULLIF(btrim(regression_conclusion),'') IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX integration_issue_tenant_status_updated_idx
|
||||
ON integration_issue (tenant_id, status, updated_at DESC, id DESC);
|
||||
CREATE INDEX integration_issue_device_idx
|
||||
ON integration_issue (tenant_id, device_id, occurred_at DESC)
|
||||
WHERE device_id IS NOT NULL;
|
||||
CREATE INDEX integration_issue_conversation_idx
|
||||
ON integration_issue (tenant_id, conversation_id, occurred_at DESC)
|
||||
WHERE conversation_id IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE integration_issue IS 'Tenant-scoped integration issue ledger required by P0282; stores only diagnostic references and resolution workflow, never transcript or recording content.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE context_snapshot
|
||||
DROP CONSTRAINT context_snapshot_preparation_failure_check,
|
||||
DROP CONSTRAINT context_snapshot_preparation_status_check,
|
||||
DROP COLUMN preparation_failure_code,
|
||||
DROP COLUMN preparation_status;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,18 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE context_snapshot
|
||||
ADD COLUMN preparation_status varchar(16) NOT NULL DEFAULT 'READY',
|
||||
ADD COLUMN preparation_failure_code varchar(80),
|
||||
ADD CONSTRAINT context_snapshot_preparation_status_check
|
||||
CHECK (preparation_status IN ('READY', 'FAILED')),
|
||||
ADD CONSTRAINT context_snapshot_preparation_failure_check
|
||||
CHECK (
|
||||
(preparation_status = 'READY' AND preparation_failure_code IS NULL)
|
||||
OR
|
||||
(preparation_status = 'FAILED' AND NULLIF(btrim(preparation_failure_code), '') IS NOT NULL)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN context_snapshot.preparation_status IS
|
||||
'Whether the authorized context was prepared. FAILED snapshots retain only a sanitized diagnostic result.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE conversation
|
||||
DROP COLUMN rtc_credential_renewal_supported;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE conversation
|
||||
ADD COLUMN rtc_credential_renewal_supported boolean NOT NULL DEFAULT false;
|
||||
|
||||
COMMENT ON COLUMN conversation.rtc_credential_renewal_supported IS
|
||||
'Frozen device capability. Only true allows renew_rtc_credential heartbeat actions; omitted v2 capability remains legacy-compatible.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX ops_config_evaluation_type_result_idx;
|
||||
|
||||
ALTER TABLE ops_config_evaluation
|
||||
DROP CONSTRAINT ops_config_evaluation_type_check,
|
||||
DROP COLUMN evidence_type;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE ops_config_evaluation
|
||||
ADD COLUMN evidence_type varchar(32) NOT NULL DEFAULT 'GENERIC',
|
||||
ADD CONSTRAINT ops_config_evaluation_type_check
|
||||
CHECK (evidence_type IN ('GENERIC', 'AGENT_QUALITY_V1'));
|
||||
|
||||
CREATE INDEX ops_config_evaluation_type_result_idx
|
||||
ON ops_config_evaluation(tenant_id, config_version_id, evidence_type, result, evaluated_at DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE runtime_config_rollout
|
||||
DROP COLUMN row_version;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE runtime_config_rollout
|
||||
ADD COLUMN row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,5 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE external_dependency_probe_state;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,25 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE external_dependency_probe_state (
|
||||
service_name varchar(32) NOT NULL CHECK (service_name ~ '^[a-z][a-z0-9-]{1,31}$'),
|
||||
dependency varchar(64) NOT NULL CHECK (dependency ~ '^[a-z][a-z0-9_]{1,63}$'),
|
||||
status varchar(16) NOT NULL CHECK (status IN ('AVAILABLE','UNAVAILABLE')),
|
||||
checked_at timestamptz NOT NULL,
|
||||
last_success_at timestamptz,
|
||||
last_failure_at timestamptz,
|
||||
last_error_code varchar(80),
|
||||
latency_ms bigint NOT NULL CHECK (latency_ms >= 0),
|
||||
probe_interval_ms bigint NOT NULL CHECK (probe_interval_ms > 0),
|
||||
consecutive_failures integer NOT NULL DEFAULT 0 CHECK (consecutive_failures >= 0),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (service_name, dependency),
|
||||
CONSTRAINT external_probe_success_check CHECK (
|
||||
(status='AVAILABLE' AND last_success_at IS NOT NULL AND last_error_code IS NULL AND consecutive_failures=0) OR
|
||||
(status='UNAVAILABLE' AND last_failure_at IS NOT NULL AND last_error_code IS NOT NULL AND consecutive_failures>0)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX external_dependency_probe_status_idx
|
||||
ON external_dependency_probe_state(status, checked_at DESC);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,11 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS integration_issue_tenant_severity_status_idx;
|
||||
ALTER TABLE integration_issue
|
||||
DROP CONSTRAINT IF EXISTS integration_issue_severity_check,
|
||||
DROP COLUMN IF EXISTS regression_evidence,
|
||||
DROP COLUMN IF EXISTS fixed_version,
|
||||
DROP COLUMN IF EXISTS root_cause,
|
||||
DROP COLUMN IF EXISTS severity;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,19 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE integration_issue
|
||||
ADD COLUMN severity varchar(16) NOT NULL DEFAULT 'MEDIUM',
|
||||
ADD COLUMN root_cause varchar(4000),
|
||||
ADD COLUMN fixed_version varchar(128),
|
||||
ADD COLUMN regression_evidence varchar(4000),
|
||||
ADD CONSTRAINT integration_issue_severity_check
|
||||
CHECK (severity IN ('LOW','MEDIUM','HIGH','CRITICAL'));
|
||||
|
||||
CREATE INDEX integration_issue_tenant_severity_status_idx
|
||||
ON integration_issue (tenant_id, severity, status, updated_at DESC, id DESC);
|
||||
|
||||
COMMENT ON COLUMN integration_issue.severity IS 'Acceptance severity used by the release-blocking summary.';
|
||||
COMMENT ON COLUMN integration_issue.root_cause IS 'Root-cause conclusion; must not contain transcript, recording content, credentials, or signed URLs.';
|
||||
COMMENT ON COLUMN integration_issue.fixed_version IS 'Candidate version or immutable build identifier containing the fix.';
|
||||
COMMENT ON COLUMN integration_issue.regression_evidence IS 'Deidentified evidence references and repeatable regression result.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS firmware_release_active_version_key;
|
||||
|
||||
ALTER TABLE firmware_release
|
||||
ADD CONSTRAINT firmware_release_version_key
|
||||
UNIQUE (tenant_id, model, hardware_revision, version);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,13 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE firmware_release
|
||||
DROP CONSTRAINT firmware_release_version_key;
|
||||
|
||||
CREATE UNIQUE INDEX firmware_release_active_version_key
|
||||
ON firmware_release (tenant_id, model, COALESCE(hardware_revision, ''), version)
|
||||
WHERE status <> 'RETIRED';
|
||||
|
||||
COMMENT ON INDEX firmware_release_active_version_key IS
|
||||
'Prevents duplicate active firmware manifests while allowing a corrected artifact to reuse the business version after an incomplete release is retired.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,5 @@
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS firmware_assignment_event;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,42 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE firmware_assignment_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
assignment_id uuid NOT NULL REFERENCES firmware_assignment(id) ON DELETE CASCADE,
|
||||
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
||||
device_id uuid NOT NULL,
|
||||
command_id uuid NOT NULL REFERENCES device_command(id),
|
||||
command_status varchar(24) NOT NULL CHECK(command_status IN ('ACKNOWLEDGED','SUCCEEDED','FAILED')),
|
||||
assignment_status varchar(24) NOT NULL CHECK(assignment_status IN ('DOWNLOADING','INSTALLING','CONFIRMED','FAILED')),
|
||||
phase text,
|
||||
progress_percent integer CHECK(progress_percent BETWEEN 0 AND 100),
|
||||
result_code text,
|
||||
active_slot text,
|
||||
target_slot text,
|
||||
previous_slot text,
|
||||
failed_slot text,
|
||||
boot_attempt_id text,
|
||||
boot_health_deadline text,
|
||||
rollback_applied boolean,
|
||||
rollback_version text,
|
||||
installed_version text,
|
||||
verified_sha256 text,
|
||||
verified_signing_key_id text,
|
||||
result_digest char(64) NOT NULL CHECK(result_digest ~ '^[0-9a-f]{64}$'),
|
||||
result_summary jsonb NOT NULL CHECK(jsonb_typeof(result_summary)='object'),
|
||||
reported_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT firmware_assignment_event_device_tenant_fk
|
||||
FOREIGN KEY(device_id,tenant_id) REFERENCES device(id,tenant_id),
|
||||
CONSTRAINT firmware_assignment_event_dedup_key
|
||||
UNIQUE(assignment_id,command_status,result_digest)
|
||||
);
|
||||
|
||||
CREATE INDEX firmware_assignment_event_timeline_idx
|
||||
ON firmware_assignment_event(assignment_id,reported_at,id);
|
||||
CREATE INDEX firmware_assignment_event_device_time_idx
|
||||
ON firmware_assignment_event(tenant_id,device_id,reported_at DESC);
|
||||
|
||||
COMMENT ON TABLE firmware_assignment_event IS
|
||||
'Immutable, deduplicated cloud receipt timeline for firmware download, staging, reboot, boot health confirmation and rollback evidence.';
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user