Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ Contributors: @bedatty, @fredcamaral, @jeffersonrodrigues92
`warmload_latency_seconds`, `get_cache_hits_total`. Tenant-id cardinality
is bounded by a configurable aggregate-rollup threshold (default 1000).
- `examples/manager/main.go` documents the canonical consumer integration.
- **Published DDL + default seed as importable artifacts.** New exported
functions `systemplane.SchemaSQL()` and `systemplane.DefaultSeedSQL()`
return, respectively, the canonical `systemplane_entries` schema DDL
(table + `systemplane_notify_v3()` function + INSERT/DELETE and UPDATE
NOTIFY triggers on the `systemplane_changes` channel) and a universal
neutral `runtime_config` default seed (`INSERT ... ON CONFLICT
(namespace, "key") DO NOTHING`). Backed by `//go:embed` of
`ddl/schema.sql` and `ddl/default_seed.sql`. This lets consumers fold
systemplane schema provisioning into their own migration pipelines
(e.g. `make systemplane-ddl` copying the artifacts into `migrations/`)
instead of relying on the lib's runtime `runSchema`. The artifacts are
static — table name `systemplane_entries` and channel `systemplane_changes`
are fixed, not parameterized. A unit test asserts the embedded schema
contains the canonical fragments the runtime emits, so a future runtime
DDL change forces the embed to be updated in lock-step.

### Changed

Expand Down
43 changes: 43 additions & 0 deletions ddl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2025 Lerian Studio.

package systemplane

import _ "embed"

// schemaSQL is the canonical systemplane schema DDL, embedded byte-faithfully
// from ddl/schema.sql. It is the same shape the runtime emits via
// internal/postgres/postgres_schema.go and internal/manager/schema.go, with
// the table name fixed to systemplane_entries and the NOTIFY channel fixed to
// systemplane_changes.
//
//go:embed ddl/schema.sql
var schemaSQL string

// defaultSeedSQL is the universal default seed for the runtime_config
// namespace, embedded from ddl/default_seed.sql. Values are neutral baselines
// inserted with ON CONFLICT (namespace, "key") DO NOTHING.
//
//go:embed ddl/default_seed.sql
var defaultSeedSQL string

// SchemaSQL returns the full systemplane schema DDL as an importable artifact.
//
// The returned SQL creates the systemplane_entries table, the
// systemplane_notify_v3() trigger function, and the INSERT/DELETE and UPDATE
// NOTIFY triggers on the systemplane_changes channel. It is idempotent and
// safe to fold into a consumer's own migration pipeline; lib-systemplane does
// not execute it for the caller.
func SchemaSQL() string {
return schemaSQL
}

// DefaultSeedSQL returns the universal default seed INSERTs as an importable
// artifact.
//
// The returned SQL seeds neutral runtime_config defaults (log level, CORS,
// rate limit, idempotency) with ON CONFLICT (namespace, "key") DO NOTHING so
// operator-set values are never overwritten. Consumers fold this into their
// own migration pipeline; lib-systemplane does not execute it for the caller.
func DefaultSeedSQL() string {
return defaultSeedSQL
}
23 changes: 23 additions & 0 deletions ddl/default_seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- systemplane universal default seed — published by lib-systemplane.
--
-- A neutral baseline for the `runtime_config` namespace that becomes every
-- consumer's default. Values are intentionally NEUTRAL (no app-specific
-- tuning, no dev origins). Every row uses
-- ON CONFLICT (namespace, "key") DO NOTHING so operator-set values are never
-- overwritten on re-application.
--
-- JSONB encoding mirrors the lib's runtime seedDefaults (json.Marshal):
-- string -> '"x"'::jsonb bool -> 'true'/'false'::jsonb int -> '5'::jsonb

INSERT INTO systemplane_entries (namespace, "key", value, updated_at, updated_by)
VALUES
('runtime_config', 'app.log_level', '"info"'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'cors.allowed_origins', '""'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'cors.allowed_methods', '"GET,POST,PUT,PATCH,DELETE,OPTIONS"'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'cors.allowed_headers', '"Origin,Content-Type,Accept,Authorization"'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'rate_limit.enabled', 'false'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'rate_limit.max', '100'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'rate_limit.expiry_sec', '60'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'idempotency.require_redis', 'false'::jsonb, now(), 'systemplane.manager'),
('runtime_config', 'idempotency.duplicate_guard_ttl_seconds', '300'::jsonb, now(), 'systemplane.manager')
ON CONFLICT (namespace, "key") DO NOTHING;
56 changes: 56 additions & 0 deletions ddl/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- systemplane schema DDL — canonical, static artifact published by lib-systemplane.
--
-- This file is byte-faithful to the runtime DDL emitted by
-- internal/postgres/postgres_schema.go (runSchema) and
-- internal/manager/schema.go (runSchema), with the table name fixed to
-- `systemplane_entries` and the NOTIFY channel fixed to `systemplane_changes`
-- (the Manager's defaultChannel). The artifact is intentionally static: it
-- carries no table/channel placeholders so consumers standardize on
-- `systemplane_entries` / `systemplane_changes` when they fold this DDL into
-- their own migration pipeline.
--
-- The DDL is fully idempotent so re-application against a populated database
-- is safe.

CREATE TABLE IF NOT EXISTS systemplane_entries (
namespace TEXT NOT NULL,
"key" TEXT NOT NULL,
value JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by TEXT NOT NULL DEFAULT '',
PRIMARY KEY (namespace, "key")
);

CREATE OR REPLACE FUNCTION systemplane_notify_v3() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM pg_notify(TG_ARGV[0], json_build_object(
'namespace', OLD.namespace,
'key', OLD.key,
'op', 'delete'
)::text);
RETURN OLD;
ELSE
PERFORM pg_notify(TG_ARGV[0], json_build_object(
'namespace', NEW.namespace,
'key', NEW.key,
'op', 'upsert'
)::text);
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS systemplane_notify_trigger ON systemplane_entries;

DROP TRIGGER IF EXISTS systemplane_notify_update_trigger ON systemplane_entries;

CREATE TRIGGER systemplane_notify_trigger
AFTER INSERT OR DELETE ON systemplane_entries
FOR EACH ROW EXECUTE FUNCTION systemplane_notify_v3('systemplane_changes');

CREATE TRIGGER systemplane_notify_update_trigger
AFTER UPDATE ON systemplane_entries
FOR EACH ROW
WHEN (OLD IS DISTINCT FROM NEW)
EXECUTE FUNCTION systemplane_notify_v3('systemplane_changes');
111 changes: 111 additions & 0 deletions ddl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//go:build unit

package systemplane_test

import (
"strings"
"testing"

systemplane "github.com/LerianStudio/lib-systemplane"
)

func TestSchemaSQL_NonEmpty(t *testing.T) {
t.Parallel()

if strings.TrimSpace(systemplane.SchemaSQL()) == "" {
t.Fatal("SchemaSQL() returned empty string")
}
}

func TestSchemaSQL_ContainsCanonicalStatements(t *testing.T) {
t.Parallel()

sql := systemplane.SchemaSQL()

// These fragments are the canonical DDL the runtime emits from
// internal/postgres/postgres_schema.go and internal/manager/schema.go.
// If a future runtime change alters the table/function/trigger shape,
// this test forces the embedded ddl/schema.sql to be updated in lock-step
// (the chosen drift-prevention strategy — see PR description).
wantFragments := []string{
"CREATE TABLE IF NOT EXISTS systemplane_entries (",
"namespace TEXT NOT NULL,",
`"key" TEXT NOT NULL,`,
"value JSONB NOT NULL,",
"updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),",
"updated_by TEXT NOT NULL DEFAULT '',",
`PRIMARY KEY (namespace, "key")`,
"CREATE OR REPLACE FUNCTION systemplane_notify_v3() RETURNS TRIGGER AS $$",
"PERFORM pg_notify(TG_ARGV[0], json_build_object(",
"'op', 'delete'",
"'op', 'upsert'",
"$$ LANGUAGE plpgsql",
"DROP TRIGGER IF EXISTS systemplane_notify_trigger ON systemplane_entries",
"DROP TRIGGER IF EXISTS systemplane_notify_update_trigger ON systemplane_entries",
"CREATE TRIGGER systemplane_notify_trigger",
"AFTER INSERT OR DELETE ON systemplane_entries",
"FOR EACH ROW EXECUTE FUNCTION systemplane_notify_v3('systemplane_changes')",
"CREATE TRIGGER systemplane_notify_update_trigger",
"AFTER UPDATE ON systemplane_entries",
"WHEN (OLD IS DISTINCT FROM NEW)",
"EXECUTE FUNCTION systemplane_notify_v3('systemplane_changes')",
}

for _, frag := range wantFragments {
if !strings.Contains(sql, frag) {
t.Errorf("SchemaSQL() missing canonical fragment:\n%q", frag)
}
}
}

func TestDefaultSeedSQL_NonEmpty(t *testing.T) {
t.Parallel()

if strings.TrimSpace(systemplane.DefaultSeedSQL()) == "" {
t.Fatal("DefaultSeedSQL() returned empty string")
}
}

func TestDefaultSeedSQL_ContainsExpectedStatements(t *testing.T) {
t.Parallel()

sql := systemplane.DefaultSeedSQL()

if !strings.Contains(sql, `INSERT INTO systemplane_entries (namespace, "key", value, updated_at, updated_by)`) {
t.Error("DefaultSeedSQL() missing INSERT header")
}

if !strings.Contains(sql, `ON CONFLICT (namespace, "key") DO NOTHING`) {
t.Error("DefaultSeedSQL() missing ON CONFLICT clause")
}

wantKeys := []struct {
key string
value string
}{
{"app.log_level", `'"info"'::jsonb`},
{"cors.allowed_origins", `'""'::jsonb`},
{"cors.allowed_methods", `'"GET,POST,PUT,PATCH,DELETE,OPTIONS"'::jsonb`},
{"cors.allowed_headers", `'"Origin,Content-Type,Accept,Authorization"'::jsonb`},
{"rate_limit.enabled", `'false'::jsonb`},
{"rate_limit.max", `'100'::jsonb`},
{"rate_limit.expiry_sec", `'60'::jsonb`},
{"idempotency.require_redis", `'false'::jsonb`},
{"idempotency.duplicate_guard_ttl_seconds", `'300'::jsonb`},
}

for _, want := range wantKeys {
if !strings.Contains(sql, want.key) {
t.Errorf("DefaultSeedSQL() missing key %q", want.key)
}

if !strings.Contains(sql, want.value) {
t.Errorf("DefaultSeedSQL() missing encoded value %q for key %q", want.value, want.key)
}
}

// Every seeded row must live in the universal runtime_config namespace.
if !strings.Contains(sql, "'runtime_config'") {
t.Error("DefaultSeedSQL() missing runtime_config namespace")
}
}
Loading