Files
pnoker f970ebc4e8 fix(mcp): complete the client credentials token issuance chain
the client_credentials grant could never complete: the authorization
insert left principal_type null, state/code/refresh columns were
declared not null for a grant that never sets them, and the connection
grant type silently fell back to authorization_code because builders
match on the enum value, not the name

- stamp the service-account principal type on the cc authorization
- bind explicit nulls for grant-specific columns in insertauthorization
- make state_hash, authorization_code_hash and the refresh hashes
  nullable in seed ddl and resync the embedded schema fingerprint
2026-09-18 09:47:13 +08:00

238 lines
13 KiB
SQL

/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* Runtime coordination tables shared by all reactive centers. */
SET search_path TO public;
CREATE TABLE IF NOT EXISTS dc3_schema_fingerprint
(
fingerprint_version SMALLINT PRIMARY KEY,
ddl_hash TEXT NOT NULL,
schema_contract TEXT NOT NULL,
id_format TEXT NOT NULL,
time_format TEXT NOT NULL,
json_format TEXT NOT NULL,
generated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE dc3_schema_fingerprint IS 'Canonical clean-DDL fingerprint used for startup validation';
COMMENT ON COLUMN dc3_schema_fingerprint.fingerprint_version IS 'Fingerprint schema version';
COMMENT ON COLUMN dc3_schema_fingerprint.ddl_hash IS 'SHA-256 hash of canonical initialization SQL';
COMMENT ON COLUMN dc3_schema_fingerprint.schema_contract IS 'Flag-day architecture contract identifier';
COMMENT ON COLUMN dc3_schema_fingerprint.id_format IS 'Identifier encoding contract';
COMMENT ON COLUMN dc3_schema_fingerprint.time_format IS 'Timestamp precision and timezone contract';
COMMENT ON COLUMN dc3_schema_fingerprint.json_format IS 'Canonical JSON encoding contract';
COMMENT ON COLUMN dc3_schema_fingerprint.generated_at IS 'Fingerprint generation timestamp in UTC';
CREATE TABLE IF NOT EXISTS dc3_operation
(
operation_id UUID PRIMARY KEY,
tenant_id BIGINT NOT NULL,
idempotency_key TEXT NOT NULL,
request_hash CHAR(64) NOT NULL,
status TEXT NOT NULL,
progress SMALLINT NOT NULL DEFAULT 0,
result JSONB,
error JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ,
CONSTRAINT chk_dc3_operation_idempotency_key CHECK (length(btrim(idempotency_key)) BETWEEN 1 AND 191),
CONSTRAINT chk_dc3_operation_progress CHECK (progress BETWEEN 0 AND 100),
CONSTRAINT chk_dc3_operation_request_hash CHECK (request_hash ~ '^[0-9a-fA-F]{64}$'),
CONSTRAINT chk_dc3_operation_status CHECK (status IN ('PENDING', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'EXPIRED')),
CONSTRAINT uq_dc3_operation_tenant_key UNIQUE (tenant_id, idempotency_key)
);
COMMENT ON TABLE dc3_operation IS 'Long-running operation state and result';
COMMENT ON COLUMN dc3_operation.operation_id IS 'UUIDv7 operation identifier';
COMMENT ON COLUMN dc3_operation.tenant_id IS 'Owning tenant identifier';
COMMENT ON COLUMN dc3_operation.idempotency_key IS 'Client idempotency key; unique within tenant';
COMMENT ON COLUMN dc3_operation.request_hash IS 'Canonical request SHA-256';
COMMENT ON COLUMN dc3_operation.status IS 'Operation lifecycle status';
COMMENT ON COLUMN dc3_operation.progress IS 'Completion percentage from 0 to 100';
COMMENT ON COLUMN dc3_operation.result IS 'Successful operation result JSON';
COMMENT ON COLUMN dc3_operation.error IS 'Failure Problem Details JSON';
COMMENT ON COLUMN dc3_operation.created_at IS 'Creation timestamp in UTC';
COMMENT ON COLUMN dc3_operation.updated_at IS 'Last update timestamp in UTC';
COMMENT ON COLUMN dc3_operation.expires_at IS 'Optional expiration timestamp in UTC';
CREATE INDEX IF NOT EXISTS idx_dc3_operation_tenant_status ON dc3_operation (tenant_id, status, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_dc3_operation_tenant_id ON dc3_operation (tenant_id, operation_id);
CREATE TABLE IF NOT EXISTS dc3_manager.dc3_device_import_job
(
operation_id UUID PRIMARY KEY,
tenant_id BIGINT NOT NULL,
driver_id BIGINT NOT NULL,
profile_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL DEFAULT 0,
operator_name TEXT NOT NULL DEFAULT '',
file_name TEXT NOT NULL,
file_data BYTEA NOT NULL,
claimed_by TEXT NOT NULL DEFAULT '',
claimed_until TIMESTAMPTZ,
attempts INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_device_import_job_operation FOREIGN KEY (tenant_id, operation_id)
REFERENCES dc3_operation (tenant_id, operation_id) ON DELETE CASCADE,
CONSTRAINT chk_device_import_job_attempts CHECK (attempts >= 0),
CONSTRAINT chk_device_import_job_file_size CHECK (octet_length(file_data) BETWEEN 1 AND 20971520)
);
CREATE INDEX IF NOT EXISTS idx_device_import_job_claim
ON dc3_manager.dc3_device_import_job (claimed_until, created_at);
COMMENT ON TABLE dc3_manager.dc3_device_import_job IS 'Durable XLSX device-import job payload';
CREATE TABLE IF NOT EXISTS dc3_outbox
(
event_id UUID PRIMARY KEY,
tenant_id BIGINT NOT NULL,
aggregate_type TEXT NOT NULL,
aggregate_id UUID,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
attempts INTEGER NOT NULL DEFAULT 0,
available_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
claimed_at TIMESTAMPTZ,
published_at TIMESTAMPTZ,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_dc3_outbox_status CHECK (status IN ('PENDING', 'CLAIMED', 'PUBLISHED', 'FAILED')),
CONSTRAINT chk_dc3_outbox_attempts CHECK (attempts >= 0)
);
CREATE INDEX IF NOT EXISTS idx_dc3_outbox_claim ON dc3_outbox (status, available_at, created_at);
CREATE INDEX IF NOT EXISTS idx_dc3_outbox_tenant ON dc3_outbox (tenant_id, created_at DESC);
COMMENT ON TABLE dc3_outbox IS 'Transactional outbox for durable event publication';
COMMENT ON COLUMN dc3_outbox.event_id IS 'UUIDv7 event identifier';
COMMENT ON COLUMN dc3_outbox.tenant_id IS 'Owning tenant identifier';
COMMENT ON COLUMN dc3_outbox.aggregate_type IS 'Aggregate type';
COMMENT ON COLUMN dc3_outbox.aggregate_id IS 'Aggregate UUID';
COMMENT ON COLUMN dc3_outbox.event_type IS 'Event type';
COMMENT ON COLUMN dc3_outbox.payload IS 'Canonical event JSON payload';
COMMENT ON COLUMN dc3_outbox.status IS 'Publication lifecycle status';
COMMENT ON COLUMN dc3_outbox.attempts IS 'Publication attempt count';
COMMENT ON COLUMN dc3_outbox.available_at IS 'Earliest retry publication time';
COMMENT ON COLUMN dc3_outbox.claimed_at IS 'Claim timestamp';
COMMENT ON COLUMN dc3_outbox.published_at IS 'Successful publication timestamp';
COMMENT ON COLUMN dc3_outbox.last_error IS 'Last publication failure';
COMMENT ON COLUMN dc3_outbox.created_at IS 'Creation timestamp in UTC';
CREATE TABLE IF NOT EXISTS dc3_idempotency
(
tenant_id BIGINT NOT NULL,
idempotency_key TEXT NOT NULL,
request_hash TEXT NOT NULL,
operation_id UUID NOT NULL,
status TEXT NOT NULL,
response JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (tenant_id, idempotency_key),
CONSTRAINT chk_dc3_idempotency_key CHECK (length(btrim(idempotency_key)) BETWEEN 1 AND 191),
CONSTRAINT chk_dc3_idempotency_request_hash CHECK (request_hash ~ '^[0-9a-fA-F]{64}$'),
CONSTRAINT chk_dc3_idempotency_status CHECK (status IN ('PENDING', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'EXPIRED')),
CONSTRAINT fk_dc3_idempotency_operation FOREIGN KEY (tenant_id, operation_id)
REFERENCES dc3_operation (tenant_id, operation_id)
);
CREATE INDEX IF NOT EXISTS idx_dc3_idempotency_expiry ON dc3_idempotency (expires_at);
COMMENT ON TABLE dc3_idempotency IS 'Tenant-scoped idempotency keys and operation results';
COMMENT ON COLUMN dc3_idempotency.tenant_id IS 'Owning tenant identifier';
COMMENT ON COLUMN dc3_idempotency.idempotency_key IS 'Client supplied idempotency key';
COMMENT ON COLUMN dc3_idempotency.request_hash IS 'Canonical request SHA-256';
COMMENT ON COLUMN dc3_idempotency.operation_id IS 'Associated operation UUID';
COMMENT ON COLUMN dc3_idempotency.status IS 'Idempotency lifecycle status';
COMMENT ON COLUMN dc3_idempotency.response IS 'Canonical response JSON';
COMMENT ON COLUMN dc3_idempotency.created_at IS 'Creation timestamp in UTC';
COMMENT ON COLUMN dc3_idempotency.expires_at IS 'Expiry timestamp in UTC';
CREATE TABLE IF NOT EXISTS dc3_platform_lock
(
lock_name TEXT PRIMARY KEY,
fencing_token BIGINT NOT NULL,
holder UUID,
expires_at TIMESTAMPTZ NOT NULL
);
COMMENT ON TABLE dc3_platform_lock IS 'Database row locks with fencing tokens';
COMMENT ON COLUMN dc3_platform_lock.lock_name IS 'Logical lock name';
COMMENT ON COLUMN dc3_platform_lock.fencing_token IS 'Monotonically increasing fencing token';
COMMENT ON COLUMN dc3_platform_lock.holder IS 'Current holder UUID';
COMMENT ON COLUMN dc3_platform_lock.expires_at IS 'Lease expiration timestamp in UTC';
-- The compose default role owns the database; deployments with a dedicated
-- runtime role can grant these tables explicitly during provisioning.
-- Durable point-value ingest receipt. A row is created before TSDB/latest writes;
-- CLAIMED leases make crash recovery deterministic and tenant-scoped message IDs
-- provide the idempotency boundary across Data Center replicas.
CREATE TABLE IF NOT EXISTS dc3_point_value_ingest_outbox
(
tenant_id BIGINT NOT NULL,
message_id TEXT NOT NULL,
schema_version INTEGER NOT NULL,
driver_node TEXT NOT NULL,
sequence BIGINT NOT NULL,
fencing_token BIGINT NOT NULL,
device_id BIGINT NOT NULL,
point_id BIGINT NOT NULL,
raw_value TEXT NOT NULL,
cal_value TEXT NOT NULL,
num_value DOUBLE PRECISION,
driver_id BIGINT NOT NULL,
create_time TIMESTAMPTZ NOT NULL,
operate_time TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
attempts INTEGER NOT NULL DEFAULT 0,
available_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
claimed_at TIMESTAMPTZ,
claimed_by TEXT,
processed_at TIMESTAMPTZ,
last_error TEXT,
PRIMARY KEY (tenant_id, message_id),
CONSTRAINT chk_point_value_outbox_status CHECK (status IN ('PENDING', 'CLAIMED', 'PERSISTED', 'PROCESSED', 'FAILED')),
CONSTRAINT chk_point_value_outbox_attempts CHECK (attempts >= 0)
);
CREATE INDEX IF NOT EXISTS idx_point_value_outbox_claim
ON dc3_point_value_ingest_outbox (status, available_at, claimed_at);
CREATE INDEX IF NOT EXISTS idx_point_value_outbox_series
ON dc3_point_value_ingest_outbox (tenant_id, device_id, point_id, create_time DESC);
COMMENT ON TABLE dc3_point_value_ingest_outbox IS 'Durable point-value ingest receipt and replay state';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.tenant_id IS 'Owning tenant ID';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.message_id IS 'Source message idempotency key';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.schema_version IS 'Payload schema version';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.driver_node IS 'Source driver node';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.sequence IS 'Source sequence number';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.fencing_token IS 'Lease fencing token';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.device_id IS 'Device ID';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.point_id IS 'Point ID';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.raw_value IS 'Raw value';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.cal_value IS 'Calibrated value';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.num_value IS 'Numeric value';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.driver_id IS 'Driver ID';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.create_time IS 'Source creation time in UTC';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.operate_time IS 'Source operation time in UTC';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.status IS 'Ingest lifecycle status';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.attempts IS 'Claim attempt count';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.available_at IS 'Earliest replay time';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.claimed_at IS 'Current claim timestamp';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.claimed_by IS 'Current claim owner';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.processed_at IS 'Alert processing completion time';
COMMENT ON COLUMN dc3_point_value_ingest_outbox.last_error IS 'Last processing error';
INSERT INTO dc3_schema_fingerprint
(fingerprint_version, ddl_hash, schema_contract, id_format, time_format, json_format)
VALUES (2, '713c2637af52d8e1e16d3d42ae8f85f48bc9e0f35d3f316509799f6a4490e136', 'r2dbc-flag-day-v1', 'uuidv7-bigint', 'utc-micros', 'canonical-v1')
ON CONFLICT (fingerprint_version) DO NOTHING;