Files

148 lines
5.7 KiB
SQL
Raw Permalink Normal View History

-- 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;