@tangen/db 0.0.0-stage → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +73 -2
  2. package/drizzle/0000_flimsy_shotgun.sql +339 -0
  3. package/drizzle/0001_app_data.sql +11 -0
  4. package/drizzle/0002_add_indexes.sql +36 -0
  5. package/drizzle/0003_extension_data_rename.sql +3 -0
  6. package/drizzle/0004_extension_data_published_globals.sql +12 -0
  7. package/drizzle/0005_auth.sql +51 -0
  8. package/drizzle/0006_auth_identities.sql +13 -0
  9. package/drizzle/0007_ai.sql +18 -0
  10. package/drizzle/0008_ai_chat_threads.sql +5 -0
  11. package/drizzle/0009_forms_source_slug.sql +11 -0
  12. package/drizzle/0010_rbac_roles_status.sql +14 -0
  13. package/drizzle/0011_jobs_v2.sql +67 -0
  14. package/drizzle/0012_search_platform.sql +36 -0
  15. package/drizzle/0013_search_synonym_label.sql +2 -0
  16. package/drizzle/0014_object_types_disabled.sql +2 -0
  17. package/drizzle/0015_playground_collections.sql +25 -0
  18. package/drizzle/0016_catchup_activity_promotion.sql +36 -0
  19. package/drizzle/0017_fields_disabled.sql +3 -0
  20. package/drizzle/0018_object_types_plural.sql +1 -0
  21. package/drizzle/0019_member_profile.sql +2 -0
  22. package/drizzle/0020_paused_jobs.sql +7 -0
  23. package/drizzle/0021_form_submission_notes.sql +2 -0
  24. package/drizzle/0022_paused_jobs_env_reason.sql +4 -0
  25. package/drizzle/0023_scale_indexes.sql +3 -0
  26. package/drizzle/0024_migration_runs.sql +24 -0
  27. package/drizzle/0025_porter_transfer.sql +30 -0
  28. package/drizzle/0026_cron_timezone.sql +1 -0
  29. package/drizzle/0027_rate_limit_counters.sql +6 -0
  30. package/drizzle/0028_realtime_events.sql +10 -0
  31. package/drizzle/0030_delivery_artifacts.sql +10 -0
  32. package/drizzle/0031_realtime_presence.sql +8 -0
  33. package/drizzle/0032_schema_pushes.sql +15 -0
  34. package/drizzle/0033_studio_design_capability.sql +37 -0
  35. package/drizzle/0034_object_types_provided_by.sql +12 -0
  36. package/drizzle/0035_custom_table_owners.sql +24 -0
  37. package/drizzle/0036_revision_version_unique.sql +42 -0
  38. package/drizzle/0037_job_queue_dedupe.sql +20 -0
  39. package/drizzle/0038_search_documents.sql +87 -0
  40. package/drizzle/0039_job_prune_indexes.sql +25 -0
  41. package/drizzle/0040_extension_data_object_idx.sql +10 -0
  42. package/drizzle/0041_knowledge.sql +79 -0
  43. package/drizzle/0042_schema_pushes_checksum.sql +7 -0
  44. package/drizzle/0043_guard_adoption.sql +12 -0
  45. package/drizzle/0044_retention_indexes.sql +7 -0
  46. package/drizzle/0045_extension_install_per_env.sql +40 -0
  47. package/drizzle/0046_extension_migrations.sql +18 -0
  48. package/drizzle/0047_environment_role.sql +25 -0
  49. package/drizzle/0048_environment_role_adoption.sql +30 -0
  50. package/drizzle/0049_release_lifecycle.sql +55 -0
  51. package/drizzle/0050_member_env_membership.sql +21 -0
  52. package/drizzle/0051_remove_environments.sql +350 -0
  53. package/drizzle/0052_release_actor_columns.sql +9 -0
  54. package/drizzle/0053_kernel_event_record.sql +26 -0
  55. package/drizzle/0054_kernel_run_identity.sql +13 -0
  56. package/drizzle/0055_kernel_identities.sql +19 -0
  57. package/drizzle/0056_forms_leave_core.sql +7 -0
  58. package/drizzle/0057_auth_leave_core.sql +8 -0
  59. package/drizzle/0058_member_grants.sql +11 -0
  60. package/drizzle/0059_extension_granted_grants.sql +7 -0
  61. package/drizzle/0060_connections.sql +21 -0
  62. package/drizzle/meta/0000_snapshot.json +1692 -0
  63. package/drizzle/meta/0002_snapshot.json +1935 -0
  64. package/drizzle/meta/0003_snapshot.json +1935 -0
  65. package/drizzle/meta/_journal.json +426 -0
  66. package/package.json +85 -4
  67. package/src/cache-keys.ts +43 -0
  68. package/src/client.ts +136 -0
  69. package/src/content-ddl.ts +70 -0
  70. package/src/content-tables.ts +110 -0
  71. package/src/custom-tables-loader.ts +65 -0
  72. package/src/ensure-system-schema.ts +195 -0
  73. package/src/extension-install-state.ts +220 -0
  74. package/src/extension-migrations.ts +270 -0
  75. package/src/generated/content-tables.ts +271 -0
  76. package/src/generated/custom-tables.ts +18 -0
  77. package/src/index.ts +24 -0
  78. package/src/instrument.ts +139 -0
  79. package/src/knowledge-schema.ts +232 -0
  80. package/src/migrate.ts +67 -0
  81. package/src/prepare-policy.ts +118 -0
  82. package/src/reconcile.ts +115 -0
  83. package/src/schema/ai-chats.ts +36 -0
  84. package/src/schema/ai-settings.ts +24 -0
  85. package/src/schema/api-keys.ts +37 -0
  86. package/src/schema/audit-log.ts +54 -0
  87. package/src/schema/connections.ts +35 -0
  88. package/src/schema/custom-table-owners.ts +32 -0
  89. package/src/schema/delivery-artifacts.ts +33 -0
  90. package/src/schema/entity-tags.ts +33 -0
  91. package/src/schema/extension-data.ts +38 -0
  92. package/src/schema/extension-migrations.ts +42 -0
  93. package/src/schema/fields.ts +37 -0
  94. package/src/schema/globals.ts +35 -0
  95. package/src/schema/identities.ts +27 -0
  96. package/src/schema/index.ts +51 -0
  97. package/src/schema/jobs.ts +104 -0
  98. package/src/schema/knowledge.ts +85 -0
  99. package/src/schema/media.ts +52 -0
  100. package/src/schema/member-grants.ts +34 -0
  101. package/src/schema/members.ts +49 -0
  102. package/src/schema/migration-runs.ts +46 -0
  103. package/src/schema/object-types.ts +41 -0
  104. package/src/schema/objects.ts +38 -0
  105. package/src/schema/paused-jobs.ts +25 -0
  106. package/src/schema/playground.ts +43 -0
  107. package/src/schema/porter-transfer.ts +47 -0
  108. package/src/schema/realtime.ts +45 -0
  109. package/src/schema/releases.ts +95 -0
  110. package/src/schema/roles.ts +27 -0
  111. package/src/schema/saved-views.ts +33 -0
  112. package/src/schema/schema-pushes.ts +43 -0
  113. package/src/schema/search-documents.ts +47 -0
  114. package/src/schema/search-query-log.ts +24 -0
  115. package/src/schema/search-recommendations.ts +27 -0
  116. package/src/schema/search-synonyms.ts +23 -0
  117. package/src/schema/webhook-deliveries.ts +37 -0
  118. package/src/schema/webhooks.ts +36 -0
  119. package/src/schema/workspace-activity.ts +35 -0
  120. package/src/schema/workspace-domains.ts +24 -0
  121. package/src/schema/workspace-installed-apps.ts +48 -0
  122. package/src/schema/workspaces.ts +24 -0
  123. package/src/schema-kit.ts +64 -0
  124. package/src/sealed.ts +84 -0
  125. package/src/seed/skeleton.ts +77 -0
  126. package/src/sql/apply-gin-indexes.ts +31 -0
  127. package/src/sql/gin-indexes.sql +11 -0
@@ -0,0 +1,79 @@
1
+ -- Knowledge (RAG) tables, owned by @tangen/db (freeze review C4).
2
+ --
3
+ -- Until this migration, @tangen/knowledge created these four tables itself at
4
+ -- runtime with CREATE TABLE IF NOT EXISTS: no ledger, no doctor, a second DDL
5
+ -- authority. The canonical text now lives in packages/db/src/knowledge-schema.ts
6
+ -- and this file carries it verbatim (the test proves it). Every statement is
7
+ -- idempotent, so an installation whose tables the runtime already created is
8
+ -- adopted as it stands: nothing is dropped, recreated or rewritten.
9
+ --
10
+ -- pgvector is an operator prerequisite (decision B): Tangen never runs
11
+ -- CREATE EXTENSION vector. knowledge_meta, knowledge_qa and knowledge_queries
12
+ -- need nothing and are created here unconditionally. knowledge_chunks needs the
13
+ -- vector type, so it is created only when that type exists; when pgvector is
14
+ -- enabled later, the canonical migration entrypoint (runMigrations, which the
15
+ -- API runs at boot) applies the same statements. The embedding column is typed
16
+ -- to the configured model dimensions by pinKnowledgeVectorSpace, the one
17
+ -- configuration-driven DDL step, also owned by @tangen/db.
18
+
19
+ CREATE TABLE IF NOT EXISTS "knowledge_meta" (
20
+ "workspace_id" text NOT NULL,
21
+ "env_id" text NOT NULL,
22
+ "provider" text NOT NULL,
23
+ "model" text NOT NULL,
24
+ "dimensions" integer NOT NULL,
25
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
26
+ PRIMARY KEY ("workspace_id", "env_id")
27
+ );
28
+ CREATE TABLE IF NOT EXISTS "knowledge_qa" (
29
+ "id" text PRIMARY KEY,
30
+ "workspace_id" text NOT NULL,
31
+ "env_id" text NOT NULL,
32
+ "question" text NOT NULL,
33
+ "answer" text NOT NULL,
34
+ "audience" text NOT NULL DEFAULT 'public',
35
+ "enabled" boolean NOT NULL DEFAULT true,
36
+ "updated_by" text,
37
+ "created_at" timestamptz NOT NULL DEFAULT now(),
38
+ "updated_at" timestamptz NOT NULL DEFAULT now()
39
+ );
40
+ CREATE INDEX IF NOT EXISTS "knowledge_qa_scope_idx" ON "knowledge_qa" ("workspace_id", "env_id");
41
+ CREATE TABLE IF NOT EXISTS "knowledge_queries" (
42
+ "id" bigserial PRIMARY KEY,
43
+ "workspace_id" text NOT NULL,
44
+ "env_id" text NOT NULL,
45
+ "query" text NOT NULL,
46
+ "source" text NOT NULL,
47
+ "audience" text NOT NULL DEFAULT 'public',
48
+ "locale" text NOT NULL DEFAULT '',
49
+ "top_score" real,
50
+ "hit_count" integer NOT NULL DEFAULT 0,
51
+ "answered" boolean NOT NULL DEFAULT false,
52
+ "created_at" timestamptz NOT NULL DEFAULT now()
53
+ );
54
+ CREATE INDEX IF NOT EXISTS "knowledge_queries_scope_idx" ON "knowledge_queries" ("workspace_id", "env_id", "created_at");
55
+
56
+ DO $$
57
+ BEGIN
58
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'vector') THEN
59
+ EXECUTE $ddl$CREATE TABLE IF NOT EXISTS "knowledge_chunks" (
60
+ "id" bigserial PRIMARY KEY,
61
+ "workspace_id" text NOT NULL,
62
+ "env_id" text NOT NULL,
63
+ "source_kind" text NOT NULL,
64
+ "source_id" text NOT NULL,
65
+ "source_group" text NOT NULL DEFAULT '',
66
+ "field" text NOT NULL DEFAULT '',
67
+ "locale" text NOT NULL DEFAULT '',
68
+ "audience" text NOT NULL DEFAULT 'public',
69
+ "chunk_index" integer NOT NULL DEFAULT 0,
70
+ "content" text NOT NULL,
71
+ "content_hash" text NOT NULL,
72
+ "metadata" jsonb,
73
+ "embedding" vector NOT NULL,
74
+ "updated_at" timestamptz NOT NULL DEFAULT now()
75
+ )$ddl$;
76
+ EXECUTE $ddl$ALTER TABLE "knowledge_chunks" ADD COLUMN IF NOT EXISTS "audience" text NOT NULL DEFAULT 'public'$ddl$;
77
+ EXECUTE $ddl$CREATE INDEX IF NOT EXISTS "knowledge_chunks_scope_idx" ON "knowledge_chunks" ("workspace_id", "env_id", "source_kind", "source_id")$ddl$;
78
+ END IF;
79
+ END $$;
@@ -0,0 +1,7 @@
1
+ -- Push history integrity (closure plan, Phase 1 Unit 4).
2
+ --
3
+ -- Every push records a checksum of the effective plan and the statements it
4
+ -- ran; the next push re-fingerprints the environment's rows from what they
5
+ -- store and refuses when one disagrees. Rows from before this column exist
6
+ -- with NULL: legacy, accepted as they are, reported, never rewritten.
7
+ ALTER TABLE "schema_pushes" ADD COLUMN IF NOT EXISTS "checksum" text;
@@ -0,0 +1,12 @@
1
+ -- Adoption of what only the boot guard ever defined (closure plan, Phase 1 Unit 5).
2
+ --
3
+ -- Until this migration, `object_types.localized` and the `entity_tags` table
4
+ -- with its two indexes existed in ensure-system-schema.ts alone: created at
5
+ -- boot and before every push by a hand-maintained guard, never by a
6
+ -- migration. The guard is now derived from the migrations and defines
7
+ -- nothing itself, so these move here, verbatim and idempotent: a database the
8
+ -- guard already provisioned is adopted as it stands.
9
+ ALTER TABLE "object_types" ADD COLUMN IF NOT EXISTS "localized" boolean NOT NULL DEFAULT false;
10
+ CREATE TABLE IF NOT EXISTS "entity_tags" ("workspace_id" text NOT NULL, "env_id" text NOT NULL, "entity_type" text NOT NULL, "entity_id" text NOT NULL, "tag" text NOT NULL, "source" text NOT NULL DEFAULT 'user', "created_at" timestamp with time zone DEFAULT now() NOT NULL, "created_by" text, CONSTRAINT "entity_tags_pk" PRIMARY KEY("workspace_id","env_id","entity_type","entity_id","tag"));
11
+ CREATE INDEX IF NOT EXISTS "entity_tags_filter_idx" ON "entity_tags" ("workspace_id","env_id","entity_type","tag");
12
+ CREATE INDEX IF NOT EXISTS "entity_tags_entity_idx" ON "entity_tags" ("workspace_id","env_id","entity_type","entity_id");
@@ -0,0 +1,7 @@
1
+ -- Retention (closure plan Phase 2, Unit 3): the daily purge deletes by
2
+ -- workspace (or target environment) and age, so every table it covers needs
3
+ -- an index on that pair. audit_log, workspace_activity and search_query_log
4
+ -- already had one; these three did not.
5
+ CREATE INDEX IF NOT EXISTS "webhook_deliveries_retention_idx" ON "webhook_deliveries" ("workspace_id", "created_at");
6
+ CREATE INDEX IF NOT EXISTS "job_runs_retention_idx" ON "job_runs" ("workspace_id", "started_at");
7
+ CREATE INDEX IF NOT EXISTS "promotion_log_retention_idx" ON "promotion_log" ("to_env_id", "promoted_at");
@@ -0,0 +1,40 @@
1
+ -- Extension installation per environment (closure plan Phase 4, Unit 1).
2
+ --
3
+ -- Until now one row meant "installed in this workspace, every environment".
4
+ -- The v1 contract installs, enables, upgrades and uninstalls per environment,
5
+ -- so the row gains env_id, state, enabled, version, upgraded_at and
6
+ -- uninstalled_at, and the old rows are ADOPTED: each workspace-wide row
7
+ -- becomes one row per environment the workspace has (from `environments`),
8
+ -- installed and enabled, options and installed_at/by carried over, version
9
+ -- left NULL until the first boot records the manifest's. A workspace with no
10
+ -- environment rows keeps one row under env_development, the platform's
11
+ -- default environment id, so nothing that was installed disappears.
12
+ -- Idempotent: only rows without an env_id are adopted; the second run finds none.
13
+ ALTER TABLE "workspace_installed_apps" ADD COLUMN IF NOT EXISTS "env_id" text;
14
+ ALTER TABLE "workspace_installed_apps" ADD COLUMN IF NOT EXISTS "state" text NOT NULL DEFAULT 'installed';
15
+ ALTER TABLE "workspace_installed_apps" ADD COLUMN IF NOT EXISTS "enabled" boolean NOT NULL DEFAULT true;
16
+ ALTER TABLE "workspace_installed_apps" ADD COLUMN IF NOT EXISTS "version" text;
17
+ ALTER TABLE "workspace_installed_apps" ADD COLUMN IF NOT EXISTS "upgraded_at" timestamp with time zone;
18
+ ALTER TABLE "workspace_installed_apps" ADD COLUMN IF NOT EXISTS "uninstalled_at" timestamp with time zone;
19
+
20
+ -- The old uniqueness (workspace, app) would refuse the second environment.
21
+ DROP INDEX IF EXISTS "workspace_installed_apps_ws_app_unique";
22
+
23
+ -- Adoption of the workspace-wide rows.
24
+ INSERT INTO "workspace_installed_apps" ("id", "workspace_id", "env_id", "app_id", "options", "state", "enabled", "version", "installed_at", "installed_by")
25
+ SELECT r."id" || '_' || replace(e."id", 'env_', ''), r."workspace_id", e."id", r."app_id", r."options", 'installed', true, r."version", r."installed_at", r."installed_by"
26
+ FROM "workspace_installed_apps" r
27
+ JOIN "environments" e ON e."workspace_id" = r."workspace_id"
28
+ WHERE r."env_id" IS NULL
29
+ ON CONFLICT DO NOTHING;
30
+
31
+ INSERT INTO "workspace_installed_apps" ("id", "workspace_id", "env_id", "app_id", "options", "state", "enabled", "version", "installed_at", "installed_by")
32
+ SELECT r."id" || '_development', r."workspace_id", 'env_development', r."app_id", r."options", 'installed', true, r."version", r."installed_at", r."installed_by"
33
+ FROM "workspace_installed_apps" r
34
+ WHERE r."env_id" IS NULL
35
+ AND NOT EXISTS (SELECT 1 FROM "environments" e WHERE e."workspace_id" = r."workspace_id")
36
+ ON CONFLICT DO NOTHING;
37
+
38
+ DELETE FROM "workspace_installed_apps" WHERE "env_id" IS NULL;
39
+
40
+ CREATE UNIQUE INDEX IF NOT EXISTS "workspace_installed_apps_ws_env_app_unique" ON "workspace_installed_apps" ("workspace_id", "env_id", "app_id");
@@ -0,0 +1,18 @@
1
+ -- The extension migration ledger (closure plan Phase 4, Unit 5): one row per
2
+ -- migration an extension applied in one workspace x environment, with the
3
+ -- checksum of the SQL that ran. Extension schema evolution is explicit,
4
+ -- deterministic and ledgered; nothing creates or alters an extension's
5
+ -- tables at import, REGISTER, ENABLE or START.
6
+ CREATE TABLE IF NOT EXISTS "extension_migrations" (
7
+ "id" text PRIMARY KEY NOT NULL,
8
+ "workspace_id" text NOT NULL,
9
+ "env_id" text NOT NULL,
10
+ "extension_id" text NOT NULL,
11
+ "migration_id" text NOT NULL,
12
+ "extension_version" text,
13
+ "checksum" text NOT NULL,
14
+ "status" text DEFAULT 'applied' NOT NULL,
15
+ "error" text,
16
+ "applied_at" timestamp with time zone DEFAULT now() NOT NULL
17
+ );
18
+ CREATE UNIQUE INDEX IF NOT EXISTS "extension_migrations_scope_unique" ON "extension_migrations" ("workspace_id","env_id","extension_id","migration_id");
@@ -0,0 +1,25 @@
1
+ -- The semantic environment role (closure plan Phase 5A, Unit 2).
2
+ --
3
+ -- Exactly three roles exist in v1 - development, staging, production - and
4
+ -- every production protection keys on the role, never on a slug or a label.
5
+ -- The column is nullable at the database level so a legacy row waits for
6
+ -- adoption (migration 0048 / doctor) instead of being guessed; a row that
7
+ -- has a role keeps it: the trigger refuses every later change. One
8
+ -- environment per role per workspace.
9
+ ALTER TABLE "environments" ADD COLUMN IF NOT EXISTS "role" text;
10
+ DO $$ BEGIN
11
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'environments_role_check') THEN
12
+ ALTER TABLE "environments" ADD CONSTRAINT "environments_role_check" CHECK ("role" IS NULL OR "role" IN ('development', 'staging', 'production'));
13
+ END IF;
14
+ END $$;
15
+ CREATE UNIQUE INDEX IF NOT EXISTS "environments_workspace_role_unique" ON "environments" ("workspace_id", "role") WHERE "role" IS NOT NULL;
16
+ CREATE OR REPLACE FUNCTION environments_role_set_once() RETURNS trigger AS $$
17
+ BEGIN
18
+ IF OLD.role IS NOT NULL AND NEW.role IS DISTINCT FROM OLD.role THEN
19
+ RAISE EXCEPTION 'environments.role is immutable: % is % and cannot become %', OLD.id, OLD.role, COALESCE(NEW.role, 'null')
20
+ USING ERRCODE = 'check_violation';
21
+ END IF;
22
+ RETURN NEW;
23
+ END $$ LANGUAGE plpgsql;
24
+ DROP TRIGGER IF EXISTS environments_role_set_once ON "environments";
25
+ CREATE TRIGGER environments_role_set_once BEFORE UPDATE ON "environments" FOR EACH ROW EXECUTE FUNCTION environments_role_set_once();
@@ -0,0 +1,30 @@
1
+ -- Adoption of legacy environments into the three roles (closure plan Phase 5A, Unit 3).
2
+ --
3
+ -- The rule is environmentRoleForSlug in @tangen/types: development | dev,
4
+ -- staging | stage, production | prod, case-insensitively. A role is assigned
5
+ -- only when exactly ONE null-role environment of the workspace names it and
6
+ -- the workspace does not already hold it. Two candidates for one role
7
+ -- (prod and production side by side), a slug the rule does not know (uat,
8
+ -- live, prod-eu) or a role already held leave the row null: doctor reports
9
+ -- it and `tangen environment adopt-role` is the operator's decision. Never
10
+ -- a guess; never two productions. Idempotent: only null roles are touched.
11
+ WITH candidates AS (
12
+ SELECT e."id", e."workspace_id",
13
+ CASE
14
+ WHEN lower(e."slug") IN ('development', 'dev') THEN 'development'
15
+ WHEN lower(e."slug") IN ('staging', 'stage') THEN 'staging'
16
+ WHEN lower(e."slug") IN ('production', 'prod') THEN 'production'
17
+ END AS "role"
18
+ FROM "environments" e
19
+ WHERE e."role" IS NULL
20
+ ), counted AS (
21
+ SELECT c."id", c."workspace_id", c."role",
22
+ count(*) OVER (PARTITION BY c."workspace_id", c."role") AS "n",
23
+ EXISTS (SELECT 1 FROM "environments" x WHERE x."workspace_id" = c."workspace_id" AND x."role" = c."role") AS "taken"
24
+ FROM candidates c
25
+ WHERE c."role" IS NOT NULL
26
+ )
27
+ UPDATE "environments" e
28
+ SET "role" = counted."role"
29
+ FROM counted
30
+ WHERE e."id" = counted."id" AND counted."n" = 1 AND NOT counted."taken";
@@ -0,0 +1,55 @@
1
+ -- Releases v1 (closure plan Phase 5B, Unit 2): the explicit state machine
2
+ -- DRAFT -> IN_REVIEW -> APPROVED -> APPLYING -> APPLIED | FAILED, the columns
3
+ -- approval, scheduling, application and failure record on, item identity
4
+ -- (one item per object per release) with a per-item outcome, and the
5
+ -- promotion history. A legacy `pending` release becomes a `draft`. Data is
6
+ -- never dropped: duplicate item selections of one object are collapsed to
7
+ -- the first (they were one application anyway).
8
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "submitted_at" timestamp with time zone;
9
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "submitted_by" text;
10
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "approved_at" timestamp with time zone;
11
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "approved_by" text;
12
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "approved_checksum" text;
13
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "scheduled_at" timestamp with time zone;
14
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "scheduled_by" text;
15
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "applying_started_at" timestamp with time zone;
16
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "failed_at" timestamp with time zone;
17
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "failure" jsonb;
18
+ ALTER TABLE "releases" ADD COLUMN IF NOT EXISTS "attempts" integer DEFAULT 0 NOT NULL;
19
+ UPDATE "releases" SET "status" = 'draft' WHERE "status" = 'pending';
20
+ DO $$ BEGIN
21
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'releases_status_check') THEN
22
+ ALTER TABLE "releases" ADD CONSTRAINT "releases_status_check" CHECK ("status" IN ('draft', 'in_review', 'approved', 'applying', 'applied', 'failed'));
23
+ END IF;
24
+ END $$;
25
+ ALTER TABLE "release_items" ADD COLUMN IF NOT EXISTS "frozen_payload" jsonb;
26
+ ALTER TABLE "release_items" ADD COLUMN IF NOT EXISTS "target_fingerprint" text;
27
+ ALTER TABLE "release_items" ADD COLUMN IF NOT EXISTS "position" integer;
28
+ ALTER TABLE "release_items" ADD COLUMN IF NOT EXISTS "apply_outcome" text;
29
+ ALTER TABLE "release_items" ADD COLUMN IF NOT EXISTS "applied_at" timestamp with time zone;
30
+ ALTER TABLE "release_items" ADD COLUMN IF NOT EXISTS "apply_error" text;
31
+ DELETE FROM "release_items" a USING "release_items" b
32
+ WHERE a."release_id" = b."release_id" AND a."type_name" = b."type_name" AND a."object_id" = b."object_id" AND a."id" > b."id";
33
+ CREATE UNIQUE INDEX IF NOT EXISTS "release_items_identity_unique" ON "release_items" ("release_id", "type_name", "object_id");
34
+ CREATE TABLE IF NOT EXISTS "release_history" (
35
+ "id" text PRIMARY KEY NOT NULL,
36
+ "release_id" text NOT NULL,
37
+ "workspace_id" text NOT NULL,
38
+ "attempt" integer DEFAULT 0 NOT NULL,
39
+ "action" text NOT NULL,
40
+ "from_status" text,
41
+ "to_status" text NOT NULL,
42
+ "actor" text,
43
+ "source_env_id" text NOT NULL,
44
+ "target_env_id" text NOT NULL,
45
+ "source_role" text,
46
+ "target_role" text,
47
+ "checksum" text,
48
+ "counts" jsonb,
49
+ "error_id" text,
50
+ "error_summary" text,
51
+ "reason" text,
52
+ "started_at" timestamp with time zone DEFAULT now() NOT NULL,
53
+ "finished_at" timestamp with time zone
54
+ );
55
+ CREATE INDEX IF NOT EXISTS "release_history_release_idx" ON "release_history" ("release_id", "started_at");
@@ -0,0 +1,21 @@
1
+ -- Environment membership (tasks/plan-environment-architecture.md D3, Stage 1
2
+ -- T1.9). A workspace member reaches an environment only through an ACTIVE row
3
+ -- here; a member is no longer implied into every environment of its database.
4
+ -- Backfilled with today's behaviour made explicit: every member active in
5
+ -- every environment of its workspace. Member and environment creation write
6
+ -- the rows from now on; the Stage 3 reconciler converges them to desired state.
7
+ CREATE TABLE IF NOT EXISTS "member_env_membership" (
8
+ "member_id" text NOT NULL REFERENCES "members"("id") ON DELETE CASCADE,
9
+ "env_id" text NOT NULL REFERENCES "environments"("id") ON DELETE CASCADE,
10
+ "workspace_id" text NOT NULL,
11
+ "status" text NOT NULL DEFAULT 'active',
12
+ "created_at" timestamp with time zone NOT NULL DEFAULT now(),
13
+ "updated_at" timestamp with time zone NOT NULL DEFAULT now(),
14
+ PRIMARY KEY ("member_id", "env_id")
15
+ );
16
+ CREATE INDEX IF NOT EXISTS "member_env_membership_ws_env_idx" ON "member_env_membership" ("workspace_id", "env_id");
17
+ INSERT INTO "member_env_membership" ("member_id", "env_id", "workspace_id", "status")
18
+ SELECT m."id", e."id", m."workspace_id", 'active'
19
+ FROM "members" m
20
+ JOIN "environments" e ON e."workspace_id" = m."workspace_id"
21
+ ON CONFLICT DO NOTHING;