177 lines
6.2 KiB
PL/PgSQL
177 lines
6.2 KiB
PL/PgSQL
CREATE TABLE IF NOT EXISTS ledger_schema_migrations (
|
|
version bigint PRIMARY KEY,
|
|
applied_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
);
|
|
|
|
CREATE TABLE ledger_accounts (
|
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
class text NOT NULL CHECK (class IN (
|
|
'USER_AVAILABLE',
|
|
'USER_FROZEN',
|
|
'EXTERNAL_BLOCKCHAIN',
|
|
'TREASURY',
|
|
'MARKET_CLEARING',
|
|
'IPG_CLEARING',
|
|
'COMMISSION_REVENUE'
|
|
)),
|
|
owner_type text NOT NULL DEFAULT '',
|
|
owner_id text NOT NULL DEFAULT '',
|
|
asset_id bigint NOT NULL CHECK (asset_id > 0),
|
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
UNIQUE (class, owner_type, owner_id, asset_id),
|
|
UNIQUE (id, asset_id),
|
|
CHECK (
|
|
class NOT IN ('USER_AVAILABLE', 'USER_FROZEN')
|
|
OR (owner_type <> '' AND owner_id <> '')
|
|
)
|
|
);
|
|
|
|
CREATE TABLE journals (
|
|
id uuid PRIMARY KEY,
|
|
source_service text NOT NULL CHECK (source_service <> ''),
|
|
idempotency_key text NOT NULL UNIQUE CHECK (idempotency_key <> ''),
|
|
source_transaction_id text NOT NULL CHECK (source_transaction_id <> ''),
|
|
tracking_code text NOT NULL DEFAULT '',
|
|
effect_kind text NOT NULL CHECK (effect_kind <> ''),
|
|
event_version integer NOT NULL CHECK (event_version > 0),
|
|
reversal_of_journal_id uuid REFERENCES journals (id),
|
|
occurred_at timestamptz NOT NULL,
|
|
recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
sealed_at timestamptz,
|
|
correlation_id text NOT NULL DEFAULT '',
|
|
actor_id text NOT NULL DEFAULT '',
|
|
blockchain_network text NOT NULL DEFAULT '',
|
|
blockchain_transaction_hash text NOT NULL DEFAULT '',
|
|
blockchain_ledger_sequence text NOT NULL DEFAULT '',
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
|
|
payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
|
|
CHECK (reversal_of_journal_id IS NULL OR reversal_of_journal_id <> id)
|
|
);
|
|
|
|
CREATE INDEX journals_source_transaction_idx
|
|
ON journals (source_service, source_transaction_id);
|
|
|
|
CREATE INDEX journals_recorded_at_idx ON journals (recorded_at, id);
|
|
|
|
CREATE UNIQUE INDEX journals_one_reversal_idx
|
|
ON journals (reversal_of_journal_id)
|
|
WHERE reversal_of_journal_id IS NOT NULL;
|
|
|
|
CREATE TABLE journal_entries (
|
|
journal_id uuid NOT NULL REFERENCES journals (id),
|
|
line_number integer NOT NULL CHECK (line_number > 0),
|
|
account_id bigint NOT NULL,
|
|
asset_id bigint NOT NULL CHECK (asset_id > 0),
|
|
amount numeric(38, 18) NOT NULL CHECK (amount <> 0),
|
|
description text NOT NULL DEFAULT '',
|
|
PRIMARY KEY (journal_id, line_number),
|
|
FOREIGN KEY (account_id, asset_id) REFERENCES ledger_accounts (id, asset_id)
|
|
);
|
|
|
|
CREATE INDEX journal_entries_account_idx
|
|
ON journal_entries (account_id, journal_id, line_number);
|
|
|
|
CREATE TABLE transaction_events (
|
|
id uuid PRIMARY KEY,
|
|
source_service text NOT NULL CHECK (source_service <> ''),
|
|
idempotency_key text NOT NULL UNIQUE CHECK (idempotency_key <> ''),
|
|
source_transaction_id text NOT NULL CHECK (source_transaction_id <> ''),
|
|
tracking_code text NOT NULL DEFAULT '',
|
|
event_version integer NOT NULL CHECK (event_version > 0),
|
|
state text NOT NULL CHECK (state IN (
|
|
'CREATED',
|
|
'PENDING_TRANSACTION',
|
|
'PENDING_ADMIN',
|
|
'SUCCESSFUL',
|
|
'FAILED',
|
|
'SUSPENDED'
|
|
)),
|
|
error_code text NOT NULL DEFAULT '',
|
|
error_message text NOT NULL DEFAULT '',
|
|
occurred_at timestamptz NOT NULL,
|
|
recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
correlation_id text NOT NULL DEFAULT '',
|
|
actor_id text NOT NULL DEFAULT '',
|
|
blockchain_network text NOT NULL DEFAULT '',
|
|
blockchain_transaction_hash text NOT NULL DEFAULT '',
|
|
blockchain_ledger_sequence text NOT NULL DEFAULT '',
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
|
|
payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$')
|
|
);
|
|
|
|
CREATE INDEX transaction_events_source_idx
|
|
ON transaction_events (source_service, source_transaction_id, event_version);
|
|
|
|
CREATE FUNCTION reject_ledger_mutation() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
RAISE EXCEPTION '% is append-only', TG_TABLE_NAME
|
|
USING ERRCODE = '55000';
|
|
END;
|
|
$$;
|
|
|
|
CREATE FUNCTION guard_journal_seal() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF OLD.sealed_at IS NOT NULL
|
|
OR NEW.sealed_at IS NULL
|
|
OR (to_jsonb(NEW) - 'sealed_at') IS DISTINCT FROM (to_jsonb(OLD) - 'sealed_at') THEN
|
|
RAISE EXCEPTION 'journals are immutable except for initial sealing'
|
|
USING ERRCODE = '55000';
|
|
END IF;
|
|
|
|
IF (SELECT count(*) FROM journal_entries WHERE journal_id = NEW.id) < 2 THEN
|
|
RAISE EXCEPTION 'journal requires at least two entries'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
|
|
IF EXISTS (
|
|
SELECT asset_id
|
|
FROM journal_entries
|
|
WHERE journal_id = NEW.id
|
|
GROUP BY asset_id
|
|
HAVING sum(amount) <> 0
|
|
) THEN
|
|
RAISE EXCEPTION 'journal is not balanced per asset'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE FUNCTION guard_entry_insert() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF (SELECT sealed_at IS NOT NULL FROM journals WHERE id = NEW.journal_id) THEN
|
|
RAISE EXCEPTION 'cannot append to a sealed journal'
|
|
USING ERRCODE = '55000';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE TRIGGER journals_guard_update
|
|
BEFORE UPDATE ON journals
|
|
FOR EACH ROW EXECUTE FUNCTION guard_journal_seal();
|
|
|
|
CREATE TRIGGER journals_reject_delete
|
|
BEFORE DELETE ON journals
|
|
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
|
|
|
CREATE TRIGGER entries_guard_insert
|
|
BEFORE INSERT ON journal_entries
|
|
FOR EACH ROW EXECUTE FUNCTION guard_entry_insert();
|
|
|
|
CREATE TRIGGER entries_reject_update_or_delete
|
|
BEFORE UPDATE OR DELETE ON journal_entries
|
|
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
|
|
|
CREATE TRIGGER accounts_reject_update_or_delete
|
|
BEFORE UPDATE OR DELETE ON ledger_accounts
|
|
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
|
|
|
CREATE TRIGGER transaction_events_reject_update_or_delete
|
|
BEFORE UPDATE OR DELETE ON transaction_events
|
|
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|