Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package database
import (
"context"
"embed"
"errors"
"fmt"
"io/fs"
"path"
"sort"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
//go:embed migrations/*.sql
var migrationFiles embed.FS
const (
defaultMaxConns = 10
defaultMinConns = 1
)
type Config struct {
URL string
MaxConns int32
MinConns int32
MaxConnIdle time.Duration
}
func NewPool(ctx context.Context, config Config) (*pgxpool.Pool, error) {
if strings.TrimSpace(config.URL) == "" {
return nil, errors.New("database URL is required")
}
poolConfig, err := pgxpool.ParseConfig(config.URL)
if err != nil {
return nil, fmt.Errorf("parse database URL: %w", err)
}
if config.MaxConns == 0 {
config.MaxConns = defaultMaxConns
}
if config.MinConns == 0 {
config.MinConns = defaultMinConns
}
if config.MaxConns < config.MinConns || config.MinConns < 0 {
return nil, errors.New("database pool limits are invalid")
}
poolConfig.MaxConns = config.MaxConns
poolConfig.MinConns = config.MinConns
if config.MaxConnIdle > 0 {
poolConfig.MaxConnIdleTime = config.MaxConnIdle
}
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return nil, fmt.Errorf("create database pool: %w", err)
}
return pool, nil
}
func Ping(ctx context.Context, pool *pgxpool.Pool) error {
if pool == nil {
return errors.New("database pool is nil")
}
if err := pool.Ping(ctx); err != nil {
return fmt.Errorf("database ping: %w", err)
}
return nil
}
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
if pool == nil {
return errors.New("database pool is nil")
}
entries, err := fs.Glob(migrationFiles, "migrations/*.sql")
if err != nil {
return fmt.Errorf("list migrations: %w", err)
}
sort.Strings(entries)
for _, entry := range entries {
if err := applyMigration(ctx, pool, entry); err != nil {
return err
}
}
return nil
}
func applyMigration(ctx context.Context, pool *pgxpool.Pool, entry string) error {
migrationID := strings.TrimSuffix(path.Base(entry), path.Ext(entry))
sqlBytes, err := migrationFiles.ReadFile(entry)
if err != nil {
return fmt.Errorf("read migration %s: %w", migrationID, err)
}
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return fmt.Errorf("begin migration %s: %w", migrationID, err)
}
defer func() { _ = tx.Rollback(ctx) }()
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('itworx-pulse:schema-migrations'))`); err != nil {
return fmt.Errorf("lock migrations: %w", err)
}
if _, err := tx.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
id text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now()
)`); err != nil {
return fmt.Errorf("create migration table: %w", err)
}
var exists bool
if err := tx.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE id = $1)`, migrationID).Scan(&exists); err != nil {
return fmt.Errorf("check migration %s: %w", migrationID, err)
}
if exists {
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit migration check %s: %w", migrationID, err)
}
return nil
}
if _, err := tx.Exec(ctx, string(sqlBytes)); err != nil {
return fmt.Errorf("apply migration %s: %w", migrationID, err)
}
if _, err := tx.Exec(ctx, `INSERT INTO schema_migrations (id) VALUES ($1)`, migrationID); err != nil {
return fmt.Errorf("record migration %s: %w", migrationID, err)
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit migration %s: %w", migrationID, err)
}
return nil
}
+302
View File
@@ -0,0 +1,302 @@
package database
import (
"context"
"os"
"strings"
"testing"
"time"
)
func TestMigrationsAreEmbeddedAndOrdered(t *testing.T) {
entries, err := migrationFiles.ReadDir("migrations")
if err != nil {
t.Fatalf("read embedded migrations: %v", err)
}
expected := []string{
"0001_foundation.sql", "0002_inventory.sql", "0003_dashboard_immutability.sql", "0004_dashboard_revision.sql",
"0005_services_probes.sql", "0006_alert_rules.sql", "0007_alert_evaluator_leases.sql", "0008_alert_state.sql",
"0009_alert_hysteresis.sql", "0010_alert_controls.sql", "0011_alert_unacknowledge.sql", "0012_notifications.sql",
"0013_incidents.sql", "0014_incident_notes.sql", "0015_entity_listing_index.sql", "0016_agent_snapshots.sql",
"0017_worker_runtime.sql", "0018_inventory_read_indexes.sql", "0019_capacity_samples.sql",
"0020_service_certificate_history_index.sql",
}
if len(entries) != len(expected) {
t.Fatalf("migration count = %d, want %d: %#v", len(entries), len(expected), entries)
}
for index, name := range expected {
if entries[index].Name() != name {
t.Fatalf("migration %d = %q, want %q", index, entries[index].Name(), name)
}
}
}
func TestServiceCertificateHistoryMigrationMatchesStatusQuery(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0020_service_certificate_history_index.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"service_certificates_service_history_idx", "service_id", "observed_at DESC", "id ASC"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("service certificate history migration is missing %q", fragment)
}
}
if strings.Contains(sql, "CONCURRENTLY") {
t.Fatal("migrations run inside a transaction and cannot create indexes concurrently")
}
}
func TestWorkerRuntimeMigrationContainsAliasAndJobLookupSafety(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0017_worker_runtime.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"CREATE TABLE container_aliases", "PRIMARY KEY (source_id, runtime_id)", "tombstoned_at", "ON DELETE CASCADE", "container_aliases_active_idx", "CREATE INDEX job_runs_recent_idx"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("worker runtime migration is missing %q", fragment)
}
}
if strings.Contains(sql, "CONCURRENTLY") {
t.Fatal("migrations run inside a transaction and cannot create indexes concurrently")
}
}
func TestAlertControlsMigrationContainsExpiryAndIndexes(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0010_alert_controls.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"CREATE TABLE alert_silences", "CREATE TABLE maintenance_windows", "expires_at", "ends_at", "status", "alert_silences_active_expiry_idx", "maintenance_windows_active_expiry_idx"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("control migration is missing %q", fragment)
}
}
}
func TestNotificationsMigrationContainsOutboxAndAuditConstraints(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0012_notifications.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"CREATE TABLE notification_channels", "CREATE TABLE notification_outbox", "CREATE TABLE notification_deliveries", "UNIQUE (outbox_id, attempt)", "ON DELETE RESTRICT", "notification_outbox_due_idx", "notification_deliveries_history_idx"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("notification migration is missing %q", fragment)
}
}
}
func TestIncidentsMigrationContainsCorrelationAndAssociationSafety(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0013_incidents.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"CREATE TABLE incidents", "incidents_active_correlation_key_uq", "CREATE TABLE incident_alerts", "CREATE TABLE incident_entities", "ON DELETE RESTRICT", "confidence", "rationale"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("incident migration is missing %q", fragment)
}
}
}
func TestIncidentNotesMigrationContainsBoundedNotes(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0014_incident_notes.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"CREATE TABLE incident_notes", "incident_notes_history_idx", "ON DELETE CASCADE", "char_length(body) BETWEEN 1 AND 2000"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("incident notes migration is missing %q", fragment)
}
}
}
func TestEntityListingMigrationIndexesKeysetPagination(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0015_entity_listing_index.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"CREATE INDEX IF NOT EXISTS entities_canonical_name_idx", "ON entities (canonical_name ASC, id ASC)"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("entity listing migration is missing %q", fragment)
}
}
if strings.Contains(sql, "CONCURRENTLY") {
t.Fatal("migrations run inside a transaction and cannot create indexes concurrently")
}
}
func TestPostgreSQLMigrationsAreRestartSafe(t *testing.T) {
dsn := os.Getenv("PULSE_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("PULSE_TEST_DATABASE_URL is not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
pool, err := NewPool(ctx, Config{URL: dsn})
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := Ping(ctx, pool); err != nil {
t.Fatal(err)
}
if err := Migrate(ctx, pool); err != nil {
t.Fatal(err)
}
if err := Migrate(ctx, pool); err != nil {
t.Fatalf("repeated migration: %v", err)
}
var count int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM schema_migrations WHERE id = '0001_foundation'`).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 1 {
t.Fatalf("migration count = %d, want 1", count)
}
var inventoryCount int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM schema_migrations WHERE id = '0002_inventory'`).Scan(&inventoryCount); err != nil {
t.Fatal(err)
}
if inventoryCount != 1 {
t.Fatalf("inventory migration count = %d, want 1", inventoryCount)
}
if _, err := pool.Exec(ctx, `INSERT INTO system_settings (key, value) VALUES ('test.persistence', '{"ok":true}') ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`); err != nil {
t.Fatal(err)
}
var value bool
if err := pool.QueryRow(ctx, `SELECT value->>'ok' = 'true' FROM system_settings WHERE key = 'test.persistence'`).Scan(&value); err != nil {
t.Fatal(err)
}
if !value {
t.Fatal("persisted setting was not retained")
}
pool.Close()
restartedPool, err := NewPool(ctx, Config{URL: dsn})
if err != nil {
t.Fatalf("reopen database pool: %v", err)
}
defer restartedPool.Close()
var afterRestart bool
if err := restartedPool.QueryRow(ctx, `SELECT value->>'ok' = 'true' FROM system_settings WHERE key = 'test.persistence'`).Scan(&afterRestart); err != nil {
t.Fatal(err)
}
if !afterRestart {
t.Fatal("persisted setting was not retained after pool restart")
}
}
func TestAlertRuleMigrationContainsVersionSafety(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0006_alert_rules.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, table := range []string{"alert_rules", "alert_rule_versions"} {
if !strings.Contains(sql, "CREATE TABLE "+table) {
t.Fatalf("migration is missing table %s", table)
}
}
for _, constraint := range []string{"UNIQUE (rule_id, version_number)", "ON DELETE RESTRICT", "DEFERRABLE INITIALLY DEFERRED", "WHERE enabled = true"} {
if !strings.Contains(sql, constraint) {
t.Fatalf("migration is missing safety constraint %q", constraint)
}
}
}
func TestAlertEvaluatorLeaseMigrationContainsExpiryIndex(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0007_alert_evaluator_leases.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"ADD COLUMN lease_owner text", "ADD COLUMN lease_until timestamptz", "CREATE INDEX job_runs_lease_idx", "status IN ('queued', 'running')"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("lease migration is missing %q", fragment)
}
}
}
func TestAlertStateMigrationContainsLifecycleAndHistorySafety(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0008_alert_state.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, table := range []string{"alert_instances", "alert_occurrences"} {
if !strings.Contains(sql, "CREATE TABLE "+table) {
t.Fatalf("migration is missing table %s", table)
}
}
for _, fragment := range []string{"UNIQUE (rule_id, fingerprint)", "UNIQUE (instance_id, evaluation_key)", "ON DELETE RESTRICT", "current_state text NOT NULL", "CREATE INDEX alert_occurrences_history_idx"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("state migration is missing %q", fragment)
}
}
}
func TestAlertHysteresisMigrationContainsCooldownSafety(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0009_alert_hysteresis.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{"ADD COLUMN cooldown_seconds", "ADD COLUMN cooldown_until", "cooldown_seconds BETWEEN 0 AND 2592000", "CREATE INDEX alert_instances_cooldown_idx"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("hysteresis migration is missing %q", fragment)
}
}
}
func TestAlertUnacknowledgeMigrationContainsConstraintSafety(t *testing.T) {
sqlBytes, err := migrationFiles.ReadFile("migrations/0011_alert_unacknowledge.sql")
if err != nil {
t.Fatal(err)
}
sql := string(sqlBytes)
for _, fragment := range []string{"DROP CONSTRAINT alert_occurrences_event_type_check", "unacknowledge", "alert_instances_acknowledged_idx"} {
if !strings.Contains(sql, fragment) {
t.Fatalf("unacknowledge migration is missing %q", fragment)
}
}
}
func TestServiceProbeMigrationContainsHistoryAndAccessSafety(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0005_services_probes.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, table := range []string{"services", "service_endpoints", "probes", "probe_results", "service_certificates", "service_dependencies", "service_permissions"} {
if !strings.Contains(sql, "CREATE TABLE "+table) {
t.Fatalf("migration is missing table %s", table)
}
}
for _, constraint := range []string{"revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0)", "ON DELETE RESTRICT", "WHERE archived_at IS NULL", "UNIQUE (probe_id, observed_at)", "permission IN ('view', 'operate', 'edit', 'admin')"} {
if !strings.Contains(sql, constraint) {
t.Fatalf("migration is missing safety constraint %q", constraint)
}
}
}
func TestAgentSnapshotMigrationBoundsPayloadAndCapability(t *testing.T) {
content, err := migrationFiles.ReadFile("migrations/0016_agent_snapshots.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, fragment := range []string{
"CREATE TABLE agent_snapshots",
"observed_at timestamptz NOT NULL",
"received_at timestamptz NOT NULL",
"jsonb_typeof(payload) = 'object'",
"pg_column_size(payload) <= 2097152",
"capability IN ('host', 'processes', 'containers', 'array', 'disks', 'pools', 'shares')",
"PRIMARY KEY (agent_id, capability)",
"agent_snapshots_capability_freshness_idx",
} {
if !strings.Contains(sql, fragment) {
t.Fatalf("agent snapshot migration is missing %q", fragment)
}
}
if strings.Contains(sql, "CONCURRENTLY") {
t.Fatal("migrations run inside a transaction and cannot create indexes concurrently")
}
}
@@ -0,0 +1,154 @@
CREATE TABLE users (
id uuid PRIMARY KEY,
external_subject text NOT NULL UNIQUE,
display_name text NOT NULL,
email text,
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
last_login_at timestamptz
);
CREATE TABLE roles (
id uuid PRIMARY KEY,
name text NOT NULL UNIQUE CHECK (name IN ('viewer', 'operator', 'editor', 'administrator')),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE user_roles (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE data_sources (
id uuid PRIMARY KEY,
type text NOT NULL,
name text NOT NULL,
enabled boolean NOT NULL DEFAULT true,
configuration_ref text NOT NULL,
capability_document jsonb NOT NULL DEFAULT '{}'::jsonb,
health_state text NOT NULL DEFAULT 'unknown' CHECK (health_state IN ('healthy', 'degraded', 'unhealthy', 'unknown')),
last_success_at timestamptz,
last_error_code text,
last_error_message text,
freshness_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE collectors (
id uuid PRIMARY KEY,
datasource_id uuid NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
kind text NOT NULL,
version text NOT NULL,
heartbeat timestamptz,
capabilities jsonb NOT NULL DEFAULT '[]'::jsonb,
status text NOT NULL DEFAULT 'unknown',
UNIQUE (datasource_id, kind)
);
CREATE TABLE entities (
id uuid PRIMARY KEY,
entity_type text NOT NULL,
canonical_name text NOT NULL,
display_name text NOT NULL,
status text NOT NULL DEFAULT 'unknown',
status_reasons jsonb NOT NULL DEFAULT '[]'::jsonb,
first_seen_at timestamptz NOT NULL,
last_seen_at timestamptz,
tombstoned_at timestamptz,
attributes jsonb NOT NULL DEFAULT '{}'::jsonb
);
CREATE TABLE entity_aliases (
entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
source_id uuid NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
external_type text NOT NULL,
external_id text NOT NULL,
PRIMARY KEY (source_id, external_type, external_id)
);
CREATE TABLE dashboards (
id uuid PRIMARY KEY,
slug text NOT NULL UNIQUE,
name text NOT NULL,
description text NOT NULL DEFAULT '',
owner_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
scope text NOT NULL CHECK (scope IN ('personal', 'shared', 'system')),
archived_at timestamptz,
current_version_id uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE dashboard_versions (
id uuid PRIMARY KEY,
dashboard_id uuid NOT NULL REFERENCES dashboards(id) ON DELETE CASCADE,
version_number integer NOT NULL CHECK (version_number > 0),
schema_version integer NOT NULL CHECK (schema_version > 0),
document jsonb NOT NULL,
change_summary text NOT NULL DEFAULT '',
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (dashboard_id, version_number)
);
ALTER TABLE dashboards
ADD CONSTRAINT dashboards_current_version_fk
FOREIGN KEY (current_version_id) REFERENCES dashboard_versions(id) ON DELETE SET NULL;
CREATE TABLE events (
id uuid PRIMARY KEY,
event_type text NOT NULL,
severity text NOT NULL,
entity_id uuid REFERENCES entities(id) ON DELETE SET NULL,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
occurred_at timestamptz NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
dedup_key text NOT NULL,
summary text NOT NULL,
attributes jsonb NOT NULL DEFAULT '{}'::jsonb,
correlation_id text,
UNIQUE (source_id, dedup_key, occurred_at)
);
CREATE TABLE audit_events (
id uuid PRIMARY KEY,
actor text NOT NULL,
action text NOT NULL,
resource_type text NOT NULL,
resource_id uuid,
result text NOT NULL,
occurred_at timestamptz NOT NULL DEFAULT now(),
correlation_id text,
before_diff jsonb,
after_diff jsonb
);
CREATE TABLE job_runs (
id uuid PRIMARY KEY,
job_type text NOT NULL,
job_key text NOT NULL,
scheduled_at timestamptz NOT NULL,
started_at timestamptz,
completed_at timestamptz,
status text NOT NULL,
counts jsonb NOT NULL DEFAULT '{}'::jsonb,
error_code text,
correlation_id text,
UNIQUE (job_type, job_key, scheduled_at)
);
CREATE TABLE system_settings (
key text PRIMARY KEY,
value jsonb NOT NULL,
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX entities_type_status_idx ON entities (entity_type, status);
CREATE INDEX events_occurred_at_idx ON events (occurred_at DESC);
CREATE INDEX audit_events_occurred_at_idx ON audit_events (occurred_at DESC);
CREATE INDEX job_runs_status_idx ON job_runs (status, scheduled_at);
@@ -0,0 +1,36 @@
CREATE TABLE entity_facts (
entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
field_name text NOT NULL,
source_id uuid NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
value jsonb NOT NULL,
observed_at timestamptz NOT NULL,
confidence numeric(5,4) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
valid_until timestamptz,
PRIMARY KEY (entity_id, field_name, source_id)
);
CREATE TABLE entity_overrides (
entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
field_name text NOT NULL,
value jsonb NOT NULL,
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (entity_id, field_name)
);
CREATE TABLE entity_relations (
id uuid PRIMARY KEY,
source_entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
relation_type text NOT NULL,
target_entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
source_id uuid NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
confidence numeric(5,4) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
confirmed boolean NOT NULL DEFAULT false,
first_seen_at timestamptz NOT NULL,
last_seen_at timestamptz,
tombstoned_at timestamptz,
UNIQUE (source_entity_id, relation_type, target_entity_id, source_id)
);
CREATE INDEX entity_facts_source_observed_idx ON entity_facts (source_id, observed_at DESC);
CREATE INDEX entity_relations_source_idx ON entity_relations (source_id, last_seen_at DESC);
@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION prevent_dashboard_version_mutation() RETURNS trigger AS $$
BEGIN
RAISE EXCEPTION 'dashboard versions are immutable';
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER dashboard_versions_immutable_update
BEFORE UPDATE ON dashboard_versions
FOR EACH ROW EXECUTE FUNCTION prevent_dashboard_version_mutation();
CREATE INDEX IF NOT EXISTS dashboard_versions_created_at_idx ON dashboard_versions (dashboard_id, created_at DESC, version_number DESC);
@@ -0,0 +1,6 @@
ALTER TABLE dashboards ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0);
CREATE INDEX IF NOT EXISTS dashboards_owner_revision_idx ON dashboards (owner_user_id, revision DESC, id ASC);
CREATE INDEX IF NOT EXISTS dashboards_name_id_idx ON dashboards (name ASC, id ASC);
CREATE INDEX IF NOT EXISTS dashboards_scope_name_id_idx ON dashboards (scope, name ASC, id ASC);
CREATE INDEX IF NOT EXISTS dashboards_owner_name_id_idx ON dashboards (owner_user_id, name ASC, id ASC);
@@ -0,0 +1,124 @@
CREATE TABLE services (
id uuid PRIMARY KEY,
entity_id uuid REFERENCES entities(id) ON DELETE SET NULL,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 160),
description text NOT NULL DEFAULT '',
state text NOT NULL DEFAULT 'unknown' CHECK (state IN ('up', 'degraded', 'down', 'unknown')),
labels jsonb NOT NULL DEFAULT '{}'::jsonb,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
archived_at timestamptz,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX services_entity_idx ON services (entity_id) WHERE archived_at IS NULL;
CREATE INDEX services_source_state_idx ON services (source_id, state, updated_at DESC);
CREATE TABLE service_endpoints (
id uuid PRIMARY KEY,
service_id uuid NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 160),
endpoint_type text NOT NULL CHECK (endpoint_type IN ('http', 'tcp', 'dns', 'icmp', 'tls')),
target jsonb NOT NULL,
enabled boolean NOT NULL DEFAULT true,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
archived_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX service_endpoints_active_idx ON service_endpoints (service_id, enabled) WHERE archived_at IS NULL;
CREATE UNIQUE INDEX service_endpoints_active_name_idx ON service_endpoints (service_id, name) WHERE archived_at IS NULL;
CREATE TABLE probes (
id uuid PRIMARY KEY,
service_id uuid NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
endpoint_id uuid REFERENCES service_endpoints(id) ON DELETE SET NULL,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 160),
probe_type text NOT NULL CHECK (probe_type IN ('http', 'tcp', 'dns', 'icmp', 'tls')),
target jsonb NOT NULL,
interval_seconds integer NOT NULL CHECK (interval_seconds BETWEEN 5 AND 86400),
timeout_seconds integer NOT NULL CHECK (timeout_seconds BETWEEN 1 AND 120),
enabled boolean NOT NULL DEFAULT true,
expected_status_codes jsonb NOT NULL DEFAULT '[]'::jsonb,
follow_redirects boolean NOT NULL DEFAULT false,
verify_tls boolean NOT NULL DEFAULT true,
content_assertion jsonb,
secret_reference text,
network_policy_id uuid,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
archived_at timestamptz,
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX probes_schedule_idx ON probes (enabled, interval_seconds, updated_at) WHERE archived_at IS NULL;
CREATE INDEX probes_service_idx ON probes (service_id, updated_at DESC);
CREATE UNIQUE INDEX probes_active_name_idx ON probes (service_id, name) WHERE archived_at IS NULL;
CREATE TABLE probe_results (
id uuid PRIMARY KEY,
probe_id uuid NOT NULL REFERENCES probes(id) ON DELETE RESTRICT,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
observed_at timestamptz NOT NULL,
completed_at timestamptz NOT NULL,
state text NOT NULL CHECK (state IN ('up', 'degraded', 'down', 'unknown')),
response_time_ms integer CHECK (response_time_ms IS NULL OR response_time_ms >= 0),
status_code integer CHECK (status_code IS NULL OR status_code BETWEEN 100 AND 599),
error_class text,
error_message text,
attributes jsonb NOT NULL DEFAULT '{}'::jsonb,
UNIQUE (probe_id, observed_at)
);
CREATE INDEX probe_results_history_idx ON probe_results (probe_id, observed_at DESC);
CREATE INDEX probe_results_state_idx ON probe_results (state, observed_at DESC);
CREATE TABLE service_certificates (
id uuid PRIMARY KEY,
service_id uuid NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
endpoint_id uuid REFERENCES service_endpoints(id) ON DELETE SET NULL,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
observed_at timestamptz NOT NULL,
expires_at timestamptz,
issuer text,
subject text,
hostname_valid boolean,
verification_state text NOT NULL CHECK (verification_state IN ('valid', 'attention', 'invalid', 'unknown')),
attributes jsonb NOT NULL DEFAULT '{}'::jsonb,
UNIQUE (service_id, endpoint_id, observed_at)
);
CREATE INDEX service_certificates_expiry_idx ON service_certificates (expires_at, observed_at DESC);
CREATE UNIQUE INDEX service_certificates_without_endpoint_unique_idx ON service_certificates (service_id, observed_at) WHERE endpoint_id IS NULL;
CREATE TABLE service_dependencies (
id uuid PRIMARY KEY,
service_id uuid NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
depends_on_service_id uuid NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
source_id uuid REFERENCES data_sources(id) ON DELETE SET NULL,
relation_type text NOT NULL CHECK (relation_type IN ('depends_on', 'backs', 'exposes')),
confidence numeric(5,4) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
confirmed boolean NOT NULL DEFAULT false,
first_seen_at timestamptz NOT NULL,
last_seen_at timestamptz,
archived_at timestamptz,
UNIQUE (service_id, depends_on_service_id, relation_type, source_id),
CHECK (service_id <> depends_on_service_id)
);
CREATE INDEX service_dependencies_source_idx ON service_dependencies (source_id, last_seen_at DESC);
CREATE UNIQUE INDEX service_dependencies_manual_unique_idx ON service_dependencies (service_id, depends_on_service_id, relation_type) WHERE source_id IS NULL;
CREATE TABLE service_permissions (
service_id uuid NOT NULL REFERENCES services(id) ON DELETE CASCADE,
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission text NOT NULL CHECK (permission IN ('view', 'operate', 'edit', 'admin')),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (service_id, role_id, permission)
);
@@ -0,0 +1,41 @@
CREATE TABLE alert_rules (
id uuid PRIMARY KEY,
schema_version integer NOT NULL CHECK (schema_version = 1),
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 160),
enabled boolean NOT NULL DEFAULT false,
severity text NOT NULL CHECK (severity IN ('attention', 'degraded', 'critical')),
scope jsonb NOT NULL DEFAULT '{}'::jsonb,
condition jsonb NOT NULL,
evaluation_interval_seconds integer NOT NULL CHECK (evaluation_interval_seconds BETWEEN 5 AND 3600),
pending_seconds integer NOT NULL CHECK (pending_seconds BETWEEN 0 AND 2592000),
resolve_seconds integer NOT NULL CHECK (resolve_seconds BETWEEN 0 AND 2592000),
unknown_behavior text NOT NULL CHECK (unknown_behavior IN ('retain-firing-as-unknown', 'become-unknown', 'ignore-short-gap')),
group_by jsonb NOT NULL DEFAULT '[]'::jsonb,
suppress_when jsonb NOT NULL DEFAULT '[]'::jsonb,
message jsonb NOT NULL,
current_version_id uuid,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE alert_rule_versions (
id uuid PRIMARY KEY,
rule_id uuid NOT NULL REFERENCES alert_rules(id) ON DELETE RESTRICT,
version_number integer NOT NULL CHECK (version_number > 0),
document jsonb NOT NULL,
change_summary text NOT NULL DEFAULT '' CHECK (length(change_summary) <= 500),
created_by uuid REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (rule_id, version_number)
);
ALTER TABLE alert_rules
ADD CONSTRAINT alert_rules_current_version_fk
FOREIGN KEY (current_version_id) REFERENCES alert_rule_versions(id)
ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX alert_rules_enabled_idx ON alert_rules (enabled, evaluation_interval_seconds, updated_at DESC) WHERE enabled = true;
CREATE INDEX alert_rules_severity_idx ON alert_rules (severity, updated_at DESC);
CREATE INDEX alert_rule_versions_history_idx ON alert_rule_versions (rule_id, version_number DESC);
@@ -0,0 +1,6 @@
ALTER TABLE job_runs
ADD COLUMN lease_owner text,
ADD COLUMN lease_until timestamptz;
CREATE INDEX job_runs_lease_idx ON job_runs (job_type, job_key, scheduled_at, lease_until)
WHERE status IN ('queued', 'running');
@@ -0,0 +1,41 @@
CREATE TABLE alert_instances (
id uuid PRIMARY KEY,
rule_id uuid NOT NULL REFERENCES alert_rules(id) ON DELETE RESTRICT,
rule_version_id uuid NOT NULL REFERENCES alert_rule_versions(id) ON DELETE RESTRICT,
fingerprint text NOT NULL CHECK (length(fingerprint) BETWEEN 1 AND 160),
entity_id uuid REFERENCES entities(id) ON DELETE RESTRICT,
current_state text NOT NULL DEFAULT 'inactive' CHECK (current_state IN ('inactive', 'pending', 'firing', 'acknowledged', 'resolved', 'unknown')),
retained_state text NOT NULL DEFAULT 'inactive' CHECK (retained_state IN ('inactive', 'pending', 'firing', 'acknowledged', 'resolved', 'unknown')),
active_since timestamptz,
recovery_since timestamptz,
last_evaluated_at timestamptz NOT NULL,
last_known_at timestamptz,
last_value jsonb NOT NULL DEFAULT 'null'::jsonb,
reason text NOT NULL DEFAULT '' CHECK (length(reason) <= 500),
source_health jsonb NOT NULL DEFAULT '{}'::jsonb,
acknowledged_by text,
acknowledged_at timestamptz,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (rule_id, fingerprint)
);
CREATE TABLE alert_occurrences (
id uuid PRIMARY KEY,
instance_id uuid NOT NULL REFERENCES alert_instances(id) ON DELETE RESTRICT,
evaluation_key text NOT NULL CHECK (length(evaluation_key) BETWEEN 1 AND 160),
event_type text NOT NULL CHECK (event_type IN ('evaluation', 'transition', 'acknowledge')),
from_state text NOT NULL CHECK (from_state IN ('inactive', 'pending', 'firing', 'acknowledged', 'resolved', 'unknown')),
to_state text NOT NULL CHECK (to_state IN ('inactive', 'pending', 'firing', 'acknowledged', 'resolved', 'unknown')),
observed_at timestamptz NOT NULL,
value jsonb NOT NULL DEFAULT 'null'::jsonb,
reason text NOT NULL DEFAULT '' CHECK (length(reason) <= 500),
source_health jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (instance_id, evaluation_key)
);
CREATE INDEX alert_instances_state_idx ON alert_instances (current_state, last_evaluated_at DESC, id ASC);
CREATE INDEX alert_instances_rule_idx ON alert_instances (rule_id, current_state, updated_at DESC, id ASC);
CREATE INDEX alert_occurrences_history_idx ON alert_occurrences (instance_id, observed_at DESC, id ASC);
@@ -0,0 +1,8 @@
ALTER TABLE alert_rules
ADD COLUMN cooldown_seconds integer NOT NULL DEFAULT 0 CHECK (cooldown_seconds BETWEEN 0 AND 2592000);
ALTER TABLE alert_instances
ADD COLUMN cooldown_until timestamptz;
CREATE INDEX alert_instances_cooldown_idx ON alert_instances (cooldown_until, current_state, id)
WHERE cooldown_until IS NOT NULL;
@@ -0,0 +1,42 @@
CREATE TABLE alert_silences (
id uuid PRIMARY KEY,
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 160),
reason text NOT NULL CHECK (char_length(reason) BETWEEN 1 AND 500),
owner text NOT NULL CHECK (char_length(owner) BETWEEN 1 AND 255),
matchers jsonb NOT NULL CHECK (jsonb_typeof(matchers) = 'object'),
starts_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL CHECK (expires_at > starts_at),
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'revoked')),
created_by text NOT NULL CHECK (char_length(created_by) BETWEEN 1 AND 255),
created_at timestamptz NOT NULL DEFAULT now(),
revoked_by text,
revoked_at timestamptz,
expired_at timestamptz,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
CHECK ((status = 'revoked' AND revoked_at IS NOT NULL) OR (status <> 'revoked' AND revoked_at IS NULL)),
CHECK ((status = 'expired' AND expired_at IS NOT NULL) OR (status <> 'expired' AND expired_at IS NULL))
);
CREATE INDEX alert_silences_active_expiry_idx ON alert_silences (expires_at, id) WHERE status = 'active';
CREATE INDEX alert_silences_listing_idx ON alert_silences (starts_at DESC, id DESC);
CREATE TABLE maintenance_windows (
id uuid PRIMARY KEY,
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 160),
reason text NOT NULL CHECK (char_length(reason) BETWEEN 1 AND 500),
selector jsonb NOT NULL CHECK (jsonb_typeof(selector) = 'object'),
starts_at timestamptz NOT NULL,
ends_at timestamptz NOT NULL CHECK (ends_at > starts_at),
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'revoked')),
created_by text NOT NULL CHECK (char_length(created_by) BETWEEN 1 AND 255),
created_at timestamptz NOT NULL DEFAULT now(),
revoked_by text,
revoked_at timestamptz,
expired_at timestamptz,
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
CHECK ((status = 'revoked' AND revoked_at IS NOT NULL) OR (status <> 'revoked' AND revoked_at IS NULL)),
CHECK ((status = 'expired' AND expired_at IS NOT NULL) OR (status <> 'expired' AND expired_at IS NULL))
);
CREATE INDEX maintenance_windows_active_expiry_idx ON maintenance_windows (ends_at, id) WHERE status = 'active';
CREATE INDEX maintenance_windows_listing_idx ON maintenance_windows (starts_at DESC, id DESC);
@@ -0,0 +1,8 @@
ALTER TABLE alert_occurrences
DROP CONSTRAINT alert_occurrences_event_type_check;
ALTER TABLE alert_occurrences
ADD CONSTRAINT alert_occurrences_event_type_check
CHECK (event_type IN ('evaluation', 'transition', 'acknowledge', 'unacknowledge'));
CREATE INDEX alert_instances_acknowledged_idx ON alert_instances (acknowledged_at DESC, id ASC) WHERE current_state = 'acknowledged';
@@ -0,0 +1,42 @@
CREATE TABLE notification_channels (
id uuid PRIMARY KEY,
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 160),
channel_type text NOT NULL CHECK (channel_type IN ('memory', 'webhook', 'email')),
enabled boolean NOT NULL DEFAULT true,
secret_ref text NOT NULL CHECK (char_length(secret_ref) BETWEEN 1 AND 255),
configuration jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(configuration) = 'object'),
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE notification_outbox (
id uuid PRIMARY KEY,
idempotency_key text NOT NULL UNIQUE CHECK (char_length(idempotency_key) BETWEEN 1 AND 255),
channel_id uuid NOT NULL REFERENCES notification_channels(id) ON DELETE RESTRICT,
event_type text NOT NULL CHECK (event_type IN ('firing', 'recovery', 'unknown')),
subject text NOT NULL CHECK (char_length(subject) BETWEEN 1 AND 240),
body text NOT NULL CHECK (char_length(body) BETWEEN 1 AND 8000),
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'delivering', 'retry', 'delivered', 'failed')),
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0 AND attempts <= 10),
next_attempt_at timestamptz NOT NULL DEFAULT now(),
locked_until timestamptz,
last_error text CHECK (last_error IS NULL OR char_length(last_error) <= 500),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
delivered_at timestamptz
);
CREATE TABLE notification_deliveries (
id uuid PRIMARY KEY,
outbox_id uuid NOT NULL REFERENCES notification_outbox(id) ON DELETE CASCADE,
attempt integer NOT NULL CHECK (attempt > 0),
status text NOT NULL CHECK (status IN ('delivering', 'delivered', 'failed')),
error text CHECK (error IS NULL OR char_length(error) <= 500),
occurred_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (outbox_id, attempt)
);
CREATE INDEX notification_outbox_due_idx ON notification_outbox (next_attempt_at, id) WHERE status IN ('pending', 'retry');
CREATE INDEX notification_outbox_channel_idx ON notification_outbox (channel_id, status, updated_at DESC, id ASC);
CREATE INDEX notification_deliveries_history_idx ON notification_deliveries (outbox_id, occurred_at DESC, id ASC);
@@ -0,0 +1,48 @@
CREATE TABLE incidents (
id uuid PRIMARY KEY,
correlation_key text NOT NULL CHECK (char_length(correlation_key) BETWEEN 1 AND 255),
title text NOT NULL CHECK (char_length(title) BETWEEN 1 AND 240),
summary text NOT NULL DEFAULT '' CHECK (char_length(summary) <= 2000),
severity text NOT NULL CHECK (severity IN ('attention', 'degraded', 'critical')),
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'acknowledged', 'resolved')),
started_at timestamptz NOT NULL,
resolved_at timestamptz,
owner_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
correlation_method text NOT NULL CHECK (char_length(correlation_method) BETWEEN 1 AND 80),
confidence numeric(4,3) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
revision bigint NOT NULL DEFAULT 1 CHECK (revision > 0),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (status <> 'resolved' OR resolved_at IS NOT NULL),
CHECK (status = 'resolved' OR resolved_at IS NULL)
);
CREATE UNIQUE INDEX incidents_active_correlation_key_uq ON incidents (correlation_key) WHERE status <> 'resolved';
CREATE INDEX incidents_list_idx ON incidents (status, severity, updated_at DESC, id ASC);
CREATE INDEX incidents_correlation_idx ON incidents (correlation_key, updated_at DESC, id ASC);
CREATE TABLE incident_alerts (
incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
alert_id uuid NOT NULL REFERENCES alert_instances(id) ON DELETE RESTRICT,
rationale text NOT NULL CHECK (char_length(rationale) BETWEEN 1 AND 500),
confidence numeric(4,3) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
correlation_method text NOT NULL CHECK (char_length(correlation_method) BETWEEN 1 AND 80),
is_manual boolean NOT NULL DEFAULT false,
added_by text NOT NULL DEFAULT '' CHECK (char_length(added_by) <= 160),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (incident_id, alert_id)
);
CREATE INDEX incident_alerts_alert_idx ON incident_alerts (alert_id, incident_id);
CREATE INDEX incident_alerts_incident_idx ON incident_alerts (incident_id, created_at ASC, alert_id ASC);
CREATE TABLE incident_entities (
incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE RESTRICT,
rationale text NOT NULL CHECK (char_length(rationale) BETWEEN 1 AND 500),
confidence numeric(4,3) NOT NULL CHECK (confidence >= 0 AND confidence <= 1),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (incident_id, entity_id)
);
CREATE INDEX incident_entities_entity_idx ON incident_entities (entity_id, incident_id);
@@ -0,0 +1,9 @@
CREATE TABLE incident_notes (
id uuid PRIMARY KEY,
incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
author text NOT NULL CHECK (char_length(author) BETWEEN 1 AND 160),
body text NOT NULL CHECK (char_length(body) BETWEEN 1 AND 2000),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX incident_notes_history_idx ON incident_notes (incident_id, created_at ASC, id ASC);
@@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS entities_canonical_name_idx ON entities (canonical_name ASC, id ASC);
@@ -0,0 +1,17 @@
-- pulse-agent writes the newest bounded telemetry snapshot per capability here and
-- pulse-api reads it. There is exactly one row per (agent, capability): history lives in
-- Prometheus, not in this table, so the transport cannot grow without bound.
CREATE TABLE agent_snapshots (
agent_id text NOT NULL CHECK (char_length(agent_id) BETWEEN 1 AND 128),
capability text NOT NULL CHECK (capability IN ('host', 'processes', 'containers', 'array', 'disks', 'pools', 'shares')),
observed_at timestamptz NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
-- The authoritative size bound is enforced by the writer against the encoded payload
-- (agentstore.MaxPayloadBytes). This check is a storage backstop against a writer that
-- bypasses the store; pg_column_size reports the stored, possibly compressed size.
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object' AND pg_column_size(payload) <= 2097152),
PRIMARY KEY (agent_id, capability)
);
-- The API reads the newest snapshot for one capability across agents on every request.
CREATE INDEX agent_snapshots_capability_freshness_idx ON agent_snapshots (capability, observed_at DESC);
@@ -0,0 +1,38 @@
-- Worker runtime support.
--
-- container_aliases is the durable memory the background discovery job needs to
-- be idempotent across restarts: reconciliation.ReconcileContainers must be able
-- to compare the current runtime snapshot against the previous one to keep a
-- stable entity identity across container recreation, and lifecycle event
-- derivation must compare the previous observed state/health/restart count to
-- decide whether anything actually changed. Both inputs are per runtime alias,
-- not per entity, so they cannot be expressed with entity_aliases (which has no
-- runtime identity or observation columns).
--
-- A runtime alias that stops being observed is tombstoned, never deleted, which
-- is what keeps a temporarily unhealthy source from erasing inventory.
CREATE TABLE container_aliases (
source_id uuid NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
runtime_id text NOT NULL CHECK (length(runtime_id) BETWEEN 1 AND 255),
entity_id uuid NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
name text NOT NULL CHECK (length(name) BETWEEN 1 AND 255),
project text NOT NULL DEFAULT '' CHECK (length(project) <= 255),
service text NOT NULL DEFAULT '' CHECK (length(service) <= 255),
image_digest text NOT NULL DEFAULT '' CHECK (length(image_digest) <= 255),
observed_state text NOT NULL DEFAULT '' CHECK (length(observed_state) <= 64),
observed_health text NOT NULL DEFAULT '' CHECK (length(observed_health) <= 64),
restart_count integer NOT NULL DEFAULT 0 CHECK (restart_count >= 0),
intentional_stop boolean NOT NULL DEFAULT false,
first_seen_at timestamptz NOT NULL,
last_seen_at timestamptz NOT NULL,
tombstoned_at timestamptz,
PRIMARY KEY (source_id, runtime_id)
);
CREATE INDEX container_aliases_entity_idx ON container_aliases (entity_id, last_seen_at DESC);
CREATE INDEX container_aliases_active_idx ON container_aliases (source_id, last_seen_at DESC) WHERE tombstoned_at IS NULL;
-- The system status endpoint reports each background job's last outcome by
-- reading the newest job_runs row per job_type. job_runs_status_idx leads with
-- status, so it cannot serve that lookup; this index can.
CREATE INDEX job_runs_recent_idx ON job_runs (job_type, scheduled_at DESC, id ASC);
@@ -0,0 +1,16 @@
CREATE INDEX IF NOT EXISTS entities_type_canonical_idx
ON entities (entity_type, canonical_name, id)
WHERE tombstoned_at IS NULL;
CREATE INDEX IF NOT EXISTS entities_status_canonical_idx
ON entities (status, canonical_name, id)
WHERE tombstoned_at IS NULL;
CREATE INDEX IF NOT EXISTS entity_relations_source_entity_idx
ON entity_relations (source_entity_id, relation_type, target_entity_id);
CREATE INDEX IF NOT EXISTS entity_relations_target_entity_idx
ON entity_relations (target_entity_id, relation_type, source_entity_id);
CREATE INDEX IF NOT EXISTS entity_facts_freshness_idx
ON entity_facts (entity_id, valid_until, field_name, observed_at DESC);
@@ -0,0 +1,14 @@
CREATE TABLE capacity_samples (
entity_kind text NOT NULL CHECK (entity_kind IN ('share', 'pool', 'disk')),
entity_id text NOT NULL CHECK (length(entity_id) BETWEEN 1 AND 128),
entity_name text NOT NULL CHECK (length(entity_name) BETWEEN 1 AND 255),
source_id text NOT NULL CHECK (length(source_id) BETWEEN 1 AND 128),
sampled_at timestamptz NOT NULL,
observed_at timestamptz NOT NULL,
used_bytes bigint NOT NULL CHECK (used_bytes >= 0),
capacity_bytes bigint NOT NULL CHECK (capacity_bytes >= 0),
PRIMARY KEY (entity_kind, entity_id, source_id, sampled_at)
);
CREATE INDEX capacity_samples_history_idx
ON capacity_samples (entity_kind, entity_id, sampled_at DESC);
@@ -0,0 +1,2 @@
CREATE INDEX service_certificates_service_history_idx
ON service_certificates (service_id, observed_at DESC, id ASC);