mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
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>
214 lines
8.6 KiB
PL/PgSQL
214 lines
8.6 KiB
PL/PgSQL
-- =============================================================================
|
||
-- 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 0–4095, got %', p_tenant_id;
|
||
END IF;
|
||
IF p_shard_id NOT BETWEEN 0 AND 1023 THEN
|
||
RAISE EXCEPTION 'shard_id must be 0–1023, 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;
|