148 lines
5.7 KiB
SQL
148 lines
5.7 KiB
SQL
-- Add fenced reconciliation claims and durable, secret-free MediaMTX path
|
|||
|
|
-- ownership/orphan reports. Runtime migrations are installed out of process.
|
||
|
|
|
||
|
|
ALTER TABLE sense.reconcile_state
|
||
|
|
ADD COLUMN IF NOT EXISTS lease_owner text,
|
||
|
|
ADD COLUMN IF NOT EXISTS lease_token text,
|
||
|
|
ADD COLUMN IF NOT EXISTS lease_until timestamptz;
|
||
|
|
|
||
|
|
DO $constraints$
|
||
|
|
BEGIN
|
||
|
|
IF NOT EXISTS (
|
||
|
|
SELECT 1 FROM pg_constraint
|
||
|
|
WHERE conrelid = 'sense.reconcile_state'::regclass
|
||
|
|
AND conname = 'sense_reconcile_lease_all_or_none'
|
||
|
|
) THEN
|
||
|
|
ALTER TABLE sense.reconcile_state
|
||
|
|
ADD CONSTRAINT sense_reconcile_lease_all_or_none CHECK (
|
||
|
|
(lease_owner IS NULL AND lease_token IS NULL AND lease_until IS NULL)
|
||
|
|
OR (btrim(lease_owner) <> '' AND btrim(lease_token) <> '' AND lease_until IS NOT NULL)
|
||
|
|
);
|
||
|
|
END IF;
|
||
|
|
END
|
||
|
|
$constraints$;
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS sense_reconcile_lease_due_idx
|
||
|
|
ON sense.reconcile_state(lease_until, next_attempt_at);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS sense.media_path_ownership (
|
||
|
|
path_name text PRIMARY KEY,
|
||
|
|
device_id text NOT NULL,
|
||
|
|
tenant_id text NOT NULL,
|
||
|
|
site_id text NOT NULL,
|
||
|
|
first_claimed_at timestamptz NOT NULL,
|
||
|
|
last_confirmed_at timestamptz NOT NULL,
|
||
|
|
CONSTRAINT sense_media_path_ownership_not_blank CHECK (
|
||
|
|
btrim(path_name) <> '' AND btrim(device_id) <> ''
|
||
|
|
AND btrim(tenant_id) <> '' AND btrim(site_id) <> ''
|
||
|
|
)
|
||
|
|
);
|
||
|
|
ALTER TABLE sense.media_path_ownership OWNER TO sense_app;
|
||
|
|
|
||
|
|
-- Keep the historical row independent from device lifecycle so a later device
|
||
|
|
-- deletion cannot erase the evidence needed to classify an owned stale path.
|
||
|
|
INSERT INTO sense.media_path_ownership(
|
||
|
|
path_name, device_id, tenant_id, site_id, first_claimed_at, last_confirmed_at
|
||
|
|
)
|
||
|
|
SELECT d.path_name, d.id, d.tenant_id, d.site_id, d.created_at, d.updated_at
|
||
|
|
FROM sense.devices d
|
||
|
|
WHERE btrim(d.path_name) <> ''
|
||
|
|
AND EXISTS (
|
||
|
|
SELECT 1 FROM sense.device_capabilities c
|
||
|
|
WHERE c.device_id = d.id AND c.capability = 'video_capture'
|
||
|
|
)
|
||
|
|
ON CONFLICT (path_name) DO UPDATE SET
|
||
|
|
device_id = EXCLUDED.device_id,
|
||
|
|
tenant_id = EXCLUDED.tenant_id,
|
||
|
|
site_id = EXCLUDED.site_id,
|
||
|
|
last_confirmed_at = GREATEST(
|
||
|
|
sense.media_path_ownership.last_confirmed_at,
|
||
|
|
EXCLUDED.last_confirmed_at
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS sense.operational_leases (
|
||
|
|
lease_name text PRIMARY KEY,
|
||
|
|
owner_id text NOT NULL,
|
||
|
|
fencing_token text NOT NULL,
|
||
|
|
lease_until timestamptz NOT NULL,
|
||
|
|
updated_at timestamptz NOT NULL,
|
||
|
|
CONSTRAINT sense_operational_lease_not_blank CHECK (
|
||
|
|
btrim(lease_name) <> '' AND btrim(owner_id) <> '' AND btrim(fencing_token) <> ''
|
||
|
|
)
|
||
|
|
);
|
||
|
|
ALTER TABLE sense.operational_leases OWNER TO sense_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS sense.orphan_scan_runs (
|
||
|
|
id text PRIMARY KEY,
|
||
|
|
instance_id text NOT NULL,
|
||
|
|
observed_count integer NOT NULL,
|
||
|
|
owned_stale_count integer NOT NULL,
|
||
|
|
unowned_count integer NOT NULL,
|
||
|
|
safety_allowed boolean NOT NULL,
|
||
|
|
safety_reason text NOT NULL,
|
||
|
|
completed_at timestamptz NOT NULL,
|
||
|
|
expires_at timestamptz NOT NULL,
|
||
|
|
CONSTRAINT sense_orphan_scan_id CHECK (id ~ '^scan_[0-9A-HJKMNP-TV-Z]{26}$'),
|
||
|
|
CONSTRAINT sense_orphan_scan_counts CHECK (
|
||
|
|
observed_count >= 0 AND owned_stale_count >= 0 AND unowned_count >= 0
|
||
|
|
AND owned_stale_count + unowned_count <= observed_count
|
||
|
|
),
|
||
|
|
CONSTRAINT sense_orphan_scan_not_blank CHECK (
|
||
|
|
btrim(instance_id) <> '' AND btrim(safety_reason) <> ''
|
||
|
|
),
|
||
|
|
CONSTRAINT sense_orphan_scan_expiry CHECK (expires_at > completed_at)
|
||
|
|
);
|
||
|
|
ALTER TABLE sense.orphan_scan_runs OWNER TO sense_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS sense.orphan_scan_findings (
|
||
|
|
scan_id text NOT NULL REFERENCES sense.orphan_scan_runs(id) ON DELETE CASCADE,
|
||
|
|
path_name text NOT NULL,
|
||
|
|
classification text NOT NULL,
|
||
|
|
device_id text,
|
||
|
|
PRIMARY KEY (scan_id, path_name),
|
||
|
|
UNIQUE (scan_id, path_name, classification),
|
||
|
|
CONSTRAINT sense_orphan_finding_classification CHECK (
|
||
|
|
classification IN ('owned_stale', 'unowned')
|
||
|
|
),
|
||
|
|
CONSTRAINT sense_orphan_finding_not_blank CHECK (
|
||
|
|
btrim(path_name) <> '' AND (device_id IS NULL OR btrim(device_id) <> '')
|
||
|
|
),
|
||
|
|
CONSTRAINT sense_orphan_finding_owner_shape CHECK (
|
||
|
|
(classification = 'owned_stale' AND device_id IS NOT NULL)
|
||
|
|
OR (classification = 'unowned' AND device_id IS NULL)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
ALTER TABLE sense.orphan_scan_findings OWNER TO sense_app;
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS sense.orphan_cleanup_actions (
|
||
|
|
scan_id text NOT NULL REFERENCES sense.orphan_scan_runs(id),
|
||
|
|
path_name text NOT NULL,
|
||
|
|
classification text NOT NULL DEFAULT 'owned_stale',
|
||
|
|
actor_id text NOT NULL,
|
||
|
|
status text NOT NULL,
|
||
|
|
error_code text,
|
||
|
|
attempted_at timestamptz NOT NULL,
|
||
|
|
PRIMARY KEY (scan_id, path_name),
|
||
|
|
FOREIGN KEY (scan_id, path_name, classification)
|
||
|
|
REFERENCES sense.orphan_scan_findings(scan_id, path_name, classification),
|
||
|
|
CONSTRAINT sense_orphan_cleanup_owned_only CHECK (classification = 'owned_stale'),
|
||
|
|
CONSTRAINT sense_orphan_cleanup_status CHECK (status IN ('deleted', 'failed')),
|
||
|
|
CONSTRAINT sense_orphan_cleanup_not_blank CHECK (
|
||
|
|
btrim(path_name) <> '' AND btrim(actor_id) <> ''
|
||
|
|
AND (error_code IS NULL OR btrim(error_code) <> '')
|
||
|
|
),
|
||
|
|
CONSTRAINT sense_orphan_cleanup_error_shape CHECK (
|
||
|
|
(status = 'deleted' AND error_code IS NULL)
|
||
|
|
OR (status = 'failed' AND error_code IS NOT NULL)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
ALTER TABLE sense.orphan_cleanup_actions OWNER TO sense_app;
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS sense_orphan_scan_completed_idx
|
||
|
|
ON sense.orphan_scan_runs(completed_at);
|
||
|
|
CREATE INDEX IF NOT EXISTS sense_orphan_cleanup_status_idx
|
||
|
|
ON sense.orphan_cleanup_actions(scan_id, status);
|
||
|
|
|
||
|
|
INSERT INTO sense.schema_migrations(version) VALUES (5)
|
||
|
|
ON CONFLICT (version) DO NOTHING;
|