prole/knoe-db/schema/ekosystem.sql
chrisfu 23cf4a8585 fix(ekosystem): register_tenant — RETURN QUERY + smallint cast
INSERT ... RETURNING needs RETURN QUERY in PL/pgSQL RETURNS TABLE functions.
tenant_id column is smallint in knoe.tenants; cast to integer to match
the function's declared return type.

Reproduced on pg.prole.org at 2026-05-30 during Phase 2 canary deploy.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-30 02:18:09 -07:00

214 lines
8.6 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =============================================================================
-- knoe.ekosystem — Stable routable base36 UUID system for federated clusters
--
-- Bit layout (82 bits → 16 base36 chars):
-- [49: timestamp_ms][12: tenant_id][10: shard_id][11: seq]
--
-- Epoch: 2026-01-01 00:00:00 UTC
-- Range: ~17,839 years (to ~19,865 AD)
-- Tenants: 4,096 (tenant_id 0 = knoe-db root authority)
-- Shards: 1,024 per tenant
-- Seq: 2,048 IDs/ms/shard (~2M IDs/sec/shard)
-- Alphabet: 0-9 a-z (base36 lowercase — safe for DNS labels and PG identifiers)
--
-- Object naming pattern: p_{16-char-uuid}_{table_name}
-- Prefix is 19 chars. PostgreSQL limit 63 bytes → table_name budget: 44 chars.
-- SQLite and ClickHouse have no meaningful limit.
--
-- LDAP integration point: knoe.tenants is the authority for access control.
-- Routing: knoe.ekosystem_tenant(table_name) extracts tenant_id from any
-- project-namespaced object name without a registry lookup.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- Tenant registry — root of trust for all ekosystems
-- knoe-db is tenant 0 and the sole issuer of tenant UUIDs.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS knoe.tenants (
tenant_id smallint PRIMARY KEY CHECK (tenant_id BETWEEN 0 AND 4095),
uuid text UNIQUE, -- 16-char ekosystem UUID; NULL for root
name text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO knoe.tenants (tenant_id, uuid, name)
VALUES (0, NULL, 'knoe-db')
ON CONFLICT DO NOTHING;
-- Monotonic counter for the seq field; nextval() is O(1) with no row locking.
-- Wraps modulo 2048 (11 bits) inside the function.
CREATE SEQUENCE IF NOT EXISTS knoe.ekosystem_seq
AS bigint MINVALUE 0 NO CYCLE;
-- ---------------------------------------------------------------------------
-- knoe.ekosystem_id(tenant_id, shard_id) → 16-char base36
--
-- Primary generator. numeric arithmetic handles the 82-bit value safely
-- since PostgreSQL bigint is capped at 63 bits.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION knoe.ekosystem_id(
p_tenant_id integer DEFAULT 0,
p_shard_id integer DEFAULT 0
) RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
c_epoch_ms constant bigint := 1767225600000; -- 2026-01-01 00:00:00 UTC
c_alphabet constant text := '0123456789abcdefghijklmnopqrstuvwxyz';
v_ts_ms bigint;
v_seq integer;
v_packed numeric;
v_result text := '';
v_rem integer;
i integer;
BEGIN
IF p_tenant_id NOT BETWEEN 0 AND 4095 THEN
RAISE EXCEPTION 'tenant_id must be 04095, got %', p_tenant_id;
END IF;
IF p_shard_id NOT BETWEEN 0 AND 1023 THEN
RAISE EXCEPTION 'shard_id must be 01023, got %', p_shard_id;
END IF;
v_ts_ms := (extract(epoch from clock_timestamp()) * 1000)::bigint - c_epoch_ms;
v_seq := nextval('knoe.ekosystem_seq')::integer % 2048;
-- [49: ts_ms | 12: tenant | 10: shard | 11: seq] packed as numeric
-- Power-of-2 multipliers avoid floating-point error in numeric division.
v_packed := v_ts_ms::numeric * 8589934592::numeric -- 2^33
+ p_tenant_id::numeric * 2097152::numeric -- 2^21
+ p_shard_id::numeric * 2048::numeric -- 2^11
+ v_seq::numeric;
FOR i IN 1..16 LOOP
v_rem := mod(v_packed, 36)::integer;
v_result := substr(c_alphabet, v_rem + 1, 1) || v_result;
v_packed := floor(v_packed / 36);
END LOOP;
RETURN v_result;
END;
$$;
-- ---------------------------------------------------------------------------
-- knoe.ekosystem_decode(id) → (tenant_id, shard_id, ts_ms, seq, issued_at)
--
-- Unpack any UUID back to its constituent fields. IMMUTABLE — safe to index.
-- Primary use: routing queries to the correct ekosystem shard without a
-- registry lookup, and LDAP principal resolution.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION knoe.ekosystem_decode(p_id text)
RETURNS TABLE(
tenant_id integer,
shard_id integer,
ts_ms bigint,
seq integer,
issued_at timestamptz
)
LANGUAGE plpgsql IMMUTABLE STRICT
AS $$
DECLARE
c_epoch_ms constant bigint := 1767225600000;
c_alphabet constant text := '0123456789abcdefghijklmnopqrstuvwxyz';
v_n numeric := 0;
v_pos integer;
i integer;
BEGIN
IF length(p_id) != 16 THEN
RAISE EXCEPTION 'ekosystem UUID must be exactly 16 chars, got %', length(p_id);
END IF;
FOR i IN 1..16 LOOP
v_pos := position(substr(p_id, i, 1) IN c_alphabet) - 1;
IF v_pos < 0 THEN
RAISE EXCEPTION 'Invalid base36 character at position %: ''%''',
i, substr(p_id, i, 1);
END IF;
v_n := v_n * 36 + v_pos;
END LOOP;
seq := mod(v_n, 2048)::integer; v_n := floor(v_n / 2048);
shard_id := mod(v_n, 1024)::integer; v_n := floor(v_n / 1024);
tenant_id := mod(v_n, 4096)::integer; v_n := floor(v_n / 4096);
ts_ms := v_n::bigint;
issued_at := to_timestamp((ts_ms + c_epoch_ms) / 1000.0);
RETURN NEXT;
END;
$$;
-- ---------------------------------------------------------------------------
-- knoe.ekosystem_tenant(table_name) → tenant_id
--
-- Extract the owning tenant from a project-namespaced object name without any
-- registry join. Extracts the UUID from position 3 (past 'p_') for 16 chars.
--
-- Usage in triggers: SELECT knoe.ekosystem_tenant(TG_TABLE_NAME)
-- Usage in routing: SELECT knoe.ekosystem_tenant('p_3k9mxqt2f8vn0r7b_accounts')
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION knoe.ekosystem_tenant(p_object_name text)
RETURNS integer
LANGUAGE sql IMMUTABLE STRICT
AS $$
SELECT tenant_id
FROM knoe.ekosystem_decode(substring(p_object_name FROM 3 FOR 16));
$$;
-- ---------------------------------------------------------------------------
-- knoe.register_tenant(name) → (tenant_id, uuid, name)
--
-- Register a new ekosystem tenant. The UUID is minted by the root authority
-- (tenant 0, shard 0) and becomes the tenant's stable external identifier —
-- used in DNS (db.{uuid}.prole.org) and service-account prefixes.
-- Subsequent IDs generated by the tenant embed their assigned tenant_id.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION knoe.register_tenant(p_name text)
RETURNS TABLE(tenant_id integer, uuid text, name text)
LANGUAGE plpgsql
AS $$
DECLARE
v_tenant_id integer;
v_uuid text;
BEGIN
SELECT COALESCE(MAX(t.tenant_id), 0) + 1
INTO v_tenant_id
FROM knoe.tenants t;
IF v_tenant_id > 4095 THEN
RAISE EXCEPTION 'Tenant capacity exhausted (max 4096 ekosystems)';
END IF;
v_uuid := knoe.ekosystem_id(0, 0); -- root authority mints the UUID
RETURN QUERY
INSERT INTO knoe.tenants (tenant_id, uuid, name)
VALUES (v_tenant_id::smallint, v_uuid, p_name)
RETURNING tenants.tenant_id::integer, tenants.uuid, tenants.name;
END;
$$;
-- ---------------------------------------------------------------------------
-- knoe.project_prefix(tenant_id, shard_id) → 'p_{uuid}_'
--
-- Convenience: generate the full 19-char prefix for a new project namespace.
-- Caller appends the table name: prefix || 'accounts'
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION knoe.project_prefix(
p_tenant_id integer DEFAULT 0,
p_shard_id integer DEFAULT 0
) RETURNS text
LANGUAGE sql
AS $$
SELECT 'p_' || knoe.ekosystem_id(p_tenant_id, p_shard_id) || '_';
$$;
-- ---------------------------------------------------------------------------
-- Grants (mirrors existing knoe schema pattern)
-- ---------------------------------------------------------------------------
GRANT EXECUTE ON FUNCTION knoe.ekosystem_id(integer, integer) TO knoe;
GRANT EXECUTE ON FUNCTION knoe.ekosystem_decode(text) TO knoe;
GRANT EXECUTE ON FUNCTION knoe.ekosystem_tenant(text) TO knoe;
GRANT EXECUTE ON FUNCTION knoe.register_tenant(text) TO knoe;
GRANT EXECUTE ON FUNCTION knoe.project_prefix(integer, integer) TO knoe;
GRANT SELECT, INSERT ON TABLE knoe.tenants TO knoe;
GRANT USAGE ON SEQUENCE knoe.ekosystem_seq TO knoe;