150 lines
6.4 KiB
SQL
150 lines
6.4 KiB
SQL
-- Sense Control API v1 durable concurrency, idempotency and batch-operation state.
|
|
|
|
ALTER TABLE sense.devices ADD COLUMN IF NOT EXISTS profile_token text NOT NULL DEFAULT '';
|
|
ALTER TABLE sense.devices ADD COLUMN IF NOT EXISTS resource_version bigint NOT NULL DEFAULT 1;
|
|
|
|
DO $area_preflight$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM sense.devices WHERE area_id IS NULL OR btrim(area_id) = '') THEN
|
|
RAISE EXCEPTION 'assign every legacy Sense device to a valid Area before installing Control API v4';
|
|
END IF;
|
|
END
|
|
$area_preflight$;
|
|
ALTER TABLE sense.devices ALTER COLUMN area_id SET NOT NULL;
|
|
|
|
DO $constraints$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint
|
|
WHERE conrelid = 'sense.devices'::regclass
|
|
AND conname = 'sense_devices_resource_version_positive'
|
|
) THEN
|
|
ALTER TABLE sense.devices ADD CONSTRAINT sense_devices_resource_version_positive
|
|
CHECK (resource_version >= 1);
|
|
END IF;
|
|
END
|
|
$constraints$;
|
|
|
|
CREATE TABLE IF NOT EXISTS sense.control_idempotency_receipts (
|
|
scope_hash bytea PRIMARY KEY,
|
|
request_hash bytea NOT NULL,
|
|
operation_name text NOT NULL,
|
|
principal_id text NOT NULL,
|
|
tenant_id text NOT NULL,
|
|
site_id text NOT NULL,
|
|
response_status integer NOT NULL,
|
|
response_body jsonb NOT NULL,
|
|
response_etag text,
|
|
response_location text,
|
|
trace_id text NOT NULL,
|
|
created_at timestamptz NOT NULL,
|
|
expires_at timestamptz NOT NULL,
|
|
CONSTRAINT sense_control_receipt_hash_lengths CHECK (
|
|
octet_length(scope_hash) = 32 AND octet_length(request_hash) = 32
|
|
),
|
|
CONSTRAINT sense_control_receipt_identity_not_blank CHECK (
|
|
btrim(operation_name) <> '' AND btrim(principal_id) <> ''
|
|
AND btrim(tenant_id) <> '' AND btrim(site_id) <> '' AND btrim(trace_id) <> ''
|
|
),
|
|
CONSTRAINT sense_control_receipt_status CHECK (response_status BETWEEN 200 AND 299),
|
|
CONSTRAINT sense_control_receipt_operation CHECK (
|
|
operation_name IN ('createDevice', 'batchSetDeviceDesiredState')
|
|
),
|
|
CONSTRAINT sense_control_receipt_lengths CHECK (
|
|
char_length(principal_id) <= 200 AND char_length(trace_id) <= 128
|
|
AND (response_etag IS NULL OR char_length(response_etag) <= 128)
|
|
AND (response_location IS NULL OR char_length(response_location) <= 512)
|
|
),
|
|
CONSTRAINT sense_control_receipt_body_object CHECK (jsonb_typeof(response_body) = 'object'),
|
|
CONSTRAINT sense_control_receipt_ttl CHECK (expires_at >= created_at + interval '24 hours')
|
|
);
|
|
ALTER TABLE sense.control_idempotency_receipts OWNER TO sense_app;
|
|
|
|
CREATE INDEX IF NOT EXISTS sense_control_receipts_expiry_idx
|
|
ON sense.control_idempotency_receipts(expires_at, scope_hash);
|
|
|
|
CREATE TABLE IF NOT EXISTS sense.batch_operations (
|
|
id text PRIMARY KEY,
|
|
tenant_id text NOT NULL,
|
|
site_id text NOT NULL,
|
|
principal_id text NOT NULL,
|
|
status text NOT NULL,
|
|
trace_id text NOT NULL,
|
|
submitted_at timestamptz NOT NULL,
|
|
completed_at timestamptz,
|
|
CONSTRAINT sense_batch_operation_id_format CHECK (
|
|
id ~ '^op_[0-9A-HJKMNP-TV-Z]{26}$'
|
|
),
|
|
CONSTRAINT sense_batch_operation_identity_not_blank CHECK (
|
|
btrim(tenant_id) <> '' AND btrim(site_id) <> ''
|
|
AND btrim(principal_id) <> '' AND btrim(trace_id) <> ''
|
|
),
|
|
CONSTRAINT sense_batch_operation_status CHECK (
|
|
status IN ('queued', 'running', 'succeeded', 'partially_succeeded', 'failed')
|
|
),
|
|
CONSTRAINT sense_batch_operation_completion CHECK (
|
|
(status IN ('queued', 'running') AND completed_at IS NULL)
|
|
OR (status IN ('succeeded', 'partially_succeeded', 'failed') AND completed_at IS NOT NULL)
|
|
)
|
|
);
|
|
ALTER TABLE sense.batch_operations OWNER TO sense_app;
|
|
|
|
CREATE TABLE IF NOT EXISTS sense.batch_operation_items (
|
|
operation_id text NOT NULL REFERENCES sense.batch_operations(id) ON DELETE CASCADE,
|
|
ordinal integer NOT NULL,
|
|
device_id text NOT NULL,
|
|
status text NOT NULL,
|
|
error_code text,
|
|
message text,
|
|
generation bigint,
|
|
PRIMARY KEY (operation_id, ordinal),
|
|
CONSTRAINT sense_batch_item_ordinal CHECK (ordinal BETWEEN 0 AND 127),
|
|
CONSTRAINT sense_batch_item_device_not_blank CHECK (btrim(device_id) <> ''),
|
|
CONSTRAINT sense_batch_item_status CHECK (
|
|
status IN ('accepted', 'rejected', 'succeeded', 'failed')
|
|
),
|
|
CONSTRAINT sense_batch_item_error_pair CHECK (
|
|
(status IN ('accepted', 'succeeded') AND error_code IS NULL AND message IS NULL)
|
|
OR (status IN ('rejected', 'failed') AND error_code IS NOT NULL AND message IS NOT NULL)
|
|
),
|
|
CONSTRAINT sense_batch_item_error_code CHECK (
|
|
error_code IS NULL OR error_code IN (
|
|
'invalid_request', 'unauthenticated', 'forbidden', 'not_found',
|
|
'conflict', 'precondition_required', 'etag_mismatch',
|
|
'idempotency_conflict', 'duplicate_serial_number', 'quota_exceeded',
|
|
'quota_projection_unavailable', 'quota_projection_invalid',
|
|
'area_policy_denied', 'area_policy_unavailable', 'adapter_not_ready',
|
|
'authentication_failed', 'endpoint_credentials_forbidden',
|
|
'batch_too_large', 'service_unavailable', 'internal_error'
|
|
)
|
|
),
|
|
CONSTRAINT sense_batch_item_message_length CHECK (
|
|
message IS NULL OR char_length(message) <= 500
|
|
),
|
|
CONSTRAINT sense_batch_item_generation CHECK (generation IS NULL OR generation >= 1)
|
|
);
|
|
ALTER TABLE sense.batch_operation_items OWNER TO sense_app;
|
|
|
|
CREATE INDEX IF NOT EXISTS sense_batch_operations_scope_idx
|
|
ON sense.batch_operations(tenant_id, site_id, submitted_at DESC, id);
|
|
|
|
ALTER TABLE sense.device_operation_outbox DROP CONSTRAINT IF EXISTS sense_outbox_event_type;
|
|
ALTER TABLE sense.device_operation_outbox ADD CONSTRAINT sense_outbox_event_type CHECK (
|
|
event_type IN (
|
|
'device.created',
|
|
'device.desired_state.accepted',
|
|
'device.configuration.accepted'
|
|
)
|
|
);
|
|
ALTER TABLE sense.device_operation_outbox DROP CONSTRAINT IF EXISTS sense_outbox_payload_kind;
|
|
ALTER TABLE sense.device_operation_outbox ADD CONSTRAINT sense_outbox_payload_kind CHECK (
|
|
(event_type = 'device.created' AND payload ->> 'kind' = 'device_created')
|
|
OR (event_type = 'device.desired_state.accepted'
|
|
AND payload ->> 'kind' = 'desired_state_accepted')
|
|
OR (event_type = 'device.configuration.accepted'
|
|
AND payload ->> 'kind' = 'configuration_accepted')
|
|
);
|
|
|
|
INSERT INTO sense.schema_migrations(version) VALUES (4)
|
|
ON CONFLICT (version) DO NOTHING;
|