chore: establish local platform baseline

This commit is contained in:
Codex Local
2026-08-26 06:41:54 +08:00
commit 29bdf4900f
575 changed files with 95644 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
BEGIN;
DROP TABLE IF EXISTS platform_metadata;
COMMIT;
+15
View File
@@ -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;
+7
View File
@@ -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;
+82
View File
@@ -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;
+24
View File
@@ -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;
+91
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
BEGIN;
DROP TABLE IF EXISTS device_claim;
COMMIT;
+27
View File
@@ -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;
+15
View File
@@ -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;
+204
View File
@@ -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;
+69
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
BEGIN;
DROP TABLE IF EXISTS export_task;
COMMIT;
+39
View File
@@ -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;
+33
View File
@@ -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;
+80
View File
@@ -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;
+175
View File
@@ -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;
+56
View File
@@ -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;
+129
View File
@@ -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;
+95
View File
@@ -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;
+38
View File
@@ -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;