@opengeni/db 0.19.0 → 0.22.1

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.
@@ -0,0 +1,2292 @@
1
+ -- deployment-mode: maintenance
2
+ -- Additive provider-neutral scoped-knowledge foundation. This migration creates
3
+ -- no connector, route, SDK/MCP/UI surface, prompt composition, policy head, or
4
+ -- active preference/memory write path.
5
+
6
+ SET lock_timeout = '5s';
7
+ SET statement_timeout = '10min';
8
+
9
+ CREATE SEQUENCE "knowledge_claim_review_revision_seq" AS bigint;
10
+
11
+ CREATE OR REPLACE FUNCTION opengeni_private.scoped_knowledge_scope_valid(
12
+ row_scope text,
13
+ row_workspace_id uuid,
14
+ row_subject_id text
15
+ ) RETURNS boolean
16
+ LANGUAGE sql IMMUTABLE AS $$
17
+ SELECT
18
+ (row_scope = 'organization' AND row_workspace_id IS NULL AND row_subject_id IS NULL)
19
+ OR (row_scope = 'workspace' AND row_workspace_id IS NOT NULL AND row_subject_id IS NULL)
20
+ OR (
21
+ row_scope = 'personal'
22
+ AND nullif(btrim(row_subject_id), '') IS NOT NULL
23
+ AND octet_length(convert_to(row_subject_id, 'UTF8')) <= 1024
24
+ );
25
+ $$;
26
+
27
+ CREATE OR REPLACE FUNCTION opengeni_private.scoped_knowledge_scope_key(
28
+ row_scope text,
29
+ row_workspace_id uuid,
30
+ row_subject_id text
31
+ ) RETURNS text
32
+ LANGUAGE sql IMMUTABLE AS $$
33
+ SELECT row_scope || ':' || coalesce(row_workspace_id::text, '-') || ':' ||
34
+ coalesce(row_subject_id, '-');
35
+ $$;
36
+
37
+ CREATE OR REPLACE FUNCTION opengeni_private.scoped_knowledge_scope_visible(
38
+ row_account_id uuid,
39
+ row_scope text,
40
+ row_workspace_id uuid,
41
+ row_subject_id text
42
+ ) RETURNS boolean
43
+ LANGUAGE sql STABLE AS $$
44
+ SELECT row_account_id = opengeni_private.current_account_id()
45
+ AND (
46
+ row_scope = 'organization'
47
+ OR (
48
+ row_scope = 'workspace'
49
+ AND row_workspace_id = opengeni_private.current_workspace_id()
50
+ )
51
+ OR (
52
+ row_scope = 'personal'
53
+ AND row_subject_id = opengeni_private.current_subject_id()
54
+ AND (
55
+ row_workspace_id IS NULL
56
+ OR row_workspace_id = opengeni_private.current_workspace_id()
57
+ )
58
+ )
59
+ );
60
+ $$;
61
+
62
+ CREATE OR REPLACE FUNCTION opengeni_private.scoped_knowledge_actor_valid(
63
+ row_actor_kind text,
64
+ row_actor_subject_id text,
65
+ row_initiating_human_subject_id text
66
+ ) RETURNS boolean
67
+ LANGUAGE sql IMMUTABLE AS $$
68
+ SELECT length(btrim(row_actor_subject_id)) BETWEEN 1 AND 1024
69
+ AND (
70
+ (
71
+ row_actor_kind = 'human'
72
+ AND row_initiating_human_subject_id = row_actor_subject_id
73
+ )
74
+ OR (
75
+ row_actor_kind = 'service'
76
+ AND (
77
+ row_initiating_human_subject_id IS NULL
78
+ OR length(btrim(row_initiating_human_subject_id)) BETWEEN 1 AND 1024
79
+ )
80
+ )
81
+ );
82
+ $$;
83
+
84
+ CREATE OR REPLACE FUNCTION opengeni_private.scoped_knowledge_actor_authorized(
85
+ row_actor_kind text,
86
+ row_actor_subject_id text,
87
+ row_initiating_human_subject_id text
88
+ ) RETURNS boolean
89
+ LANGUAGE sql STABLE AS $$
90
+ SELECT opengeni_private.scoped_knowledge_actor_valid(
91
+ row_actor_kind, row_actor_subject_id, row_initiating_human_subject_id
92
+ )
93
+ AND (
94
+ (
95
+ row_actor_kind = 'human'
96
+ AND row_actor_subject_id = opengeni_private.current_subject_id()
97
+ AND row_initiating_human_subject_id = opengeni_private.current_subject_id()
98
+ )
99
+ OR (
100
+ row_actor_kind = 'service'
101
+ AND row_initiating_human_subject_id
102
+ IS NOT DISTINCT FROM opengeni_private.current_subject_id()
103
+ )
104
+ );
105
+ $$;
106
+
107
+ CREATE TABLE "knowledge_operation_receipts" (
108
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
109
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
110
+ "scope_kind" text NOT NULL,
111
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
112
+ "scope_subject_id" text,
113
+ "scope_key" text NOT NULL,
114
+ "operation_kind" text NOT NULL,
115
+ "operation_namespace" text NOT NULL,
116
+ "operation_id" text NOT NULL,
117
+ "input_hash" text NOT NULL,
118
+ "result_id" uuid NOT NULL,
119
+ "actor_kind" text NOT NULL,
120
+ "actor_subject_id" text NOT NULL,
121
+ "initiating_human_subject_id" text,
122
+ "created_at" timestamptz NOT NULL DEFAULT now(),
123
+ CONSTRAINT "knowledge_operation_receipts_scope_chk" CHECK (
124
+ opengeni_private.scoped_knowledge_scope_valid(
125
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
126
+ )
127
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
128
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
129
+ )
130
+ ),
131
+ CONSTRAINT "knowledge_operation_receipts_identity_chk" CHECK (
132
+ "operation_kind" IN (
133
+ 'provider', 'source', 'source_object', 'document_version',
134
+ 'entity', 'entity_alias', 'fact', 'claim_relation'
135
+ )
136
+ AND length(btrim("operation_namespace")) BETWEEN 1 AND 256
137
+ AND length(btrim("operation_id")) BETWEEN 1 AND 256
138
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
139
+ ),
140
+ CONSTRAINT "knowledge_operation_receipts_actor_chk" CHECK (
141
+ opengeni_private.scoped_knowledge_actor_valid(
142
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
143
+ )
144
+ ),
145
+ CONSTRAINT "knowledge_operation_receipts_operation_uq"
146
+ UNIQUE ("account_id", "operation_kind", "operation_namespace", "operation_id")
147
+ );
148
+
149
+ CREATE INDEX "knowledge_operation_receipts_result_idx"
150
+ ON "knowledge_operation_receipts" ("operation_kind", "result_id");
151
+
152
+ CREATE TABLE "knowledge_providers" (
153
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
154
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
155
+ "scope_kind" text NOT NULL,
156
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
157
+ "scope_subject_id" text,
158
+ "scope_key" text NOT NULL,
159
+ "provider_key" text NOT NULL,
160
+ "external_tenant_id" text NOT NULL,
161
+ "lifecycle_state" text NOT NULL DEFAULT 'active',
162
+ "lifecycle_generation" bigint NOT NULL DEFAULT 1,
163
+ "operation_id" text NOT NULL,
164
+ "input_hash" text NOT NULL,
165
+ "actor_kind" text NOT NULL,
166
+ "actor_subject_id" text NOT NULL,
167
+ "initiating_human_subject_id" text,
168
+ "created_at" timestamptz NOT NULL DEFAULT now(),
169
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
170
+ CONSTRAINT "knowledge_providers_scope_chk" CHECK (
171
+ opengeni_private.scoped_knowledge_scope_valid(
172
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
173
+ )
174
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
175
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
176
+ )
177
+ ),
178
+ CONSTRAINT "knowledge_providers_provider_key_chk" CHECK (
179
+ "provider_key" = lower(btrim("provider_key"))
180
+ AND length("provider_key") BETWEEN 1 AND 96
181
+ AND "provider_key" ~ '^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$'
182
+ AND "provider_key" !~ '--'
183
+ ),
184
+ CONSTRAINT "knowledge_providers_external_identity_chk" CHECK (
185
+ length(btrim("external_tenant_id")) BETWEEN 1 AND 1024
186
+ ),
187
+ CONSTRAINT "knowledge_providers_lifecycle_chk" CHECK (
188
+ "lifecycle_state" IN ('active', 'deleted', 'revoked')
189
+ AND "lifecycle_generation" > 0
190
+ ),
191
+ CONSTRAINT "knowledge_providers_idempotency_chk" CHECK (
192
+ length(btrim("operation_id")) BETWEEN 1 AND 256
193
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
194
+ ),
195
+ CONSTRAINT "knowledge_providers_actor_chk" CHECK (
196
+ opengeni_private.scoped_knowledge_actor_valid(
197
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
198
+ )
199
+ ),
200
+ CONSTRAINT "knowledge_providers_external_identity_uq"
201
+ UNIQUE ("account_id", "provider_key", "external_tenant_id"),
202
+ CONSTRAINT "knowledge_providers_operation_uq" UNIQUE ("account_id", "operation_id"),
203
+ CONSTRAINT "knowledge_providers_scope_identity_uq"
204
+ UNIQUE ("account_id", "id", "scope_key")
205
+ );
206
+
207
+ CREATE INDEX "knowledge_providers_scope_lifecycle_idx"
208
+ ON "knowledge_providers" ("account_id", "scope_key", "lifecycle_state");
209
+
210
+ CREATE TABLE "knowledge_sources" (
211
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
212
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
213
+ "scope_kind" text NOT NULL,
214
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
215
+ "scope_subject_id" text,
216
+ "scope_key" text NOT NULL,
217
+ "provider_id" uuid NOT NULL,
218
+ "external_source_id" text NOT NULL,
219
+ "source_kind" text NOT NULL,
220
+ "source_uri" text,
221
+ "current_acl_generation" bigint,
222
+ "sync_generation" bigint NOT NULL DEFAULT 0,
223
+ "sync_cursor" text,
224
+ "lifecycle_state" text NOT NULL DEFAULT 'active',
225
+ "lifecycle_generation" bigint NOT NULL DEFAULT 1,
226
+ "operation_id" text NOT NULL,
227
+ "input_hash" text NOT NULL,
228
+ "actor_kind" text NOT NULL,
229
+ "actor_subject_id" text NOT NULL,
230
+ "initiating_human_subject_id" text,
231
+ "created_at" timestamptz NOT NULL DEFAULT now(),
232
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
233
+ CONSTRAINT "knowledge_sources_provider_fk"
234
+ FOREIGN KEY ("account_id", "provider_id", "scope_key")
235
+ REFERENCES "knowledge_providers"("account_id", "id", "scope_key") ON DELETE RESTRICT,
236
+ CONSTRAINT "knowledge_sources_scope_chk" CHECK (
237
+ opengeni_private.scoped_knowledge_scope_valid(
238
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
239
+ )
240
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
241
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
242
+ )
243
+ ),
244
+ CONSTRAINT "knowledge_sources_identity_chk" CHECK (
245
+ length(btrim("external_source_id")) BETWEEN 1 AND 1024
246
+ AND length(btrim("source_kind")) BETWEEN 1 AND 96
247
+ AND ("source_uri" IS NULL OR length("source_uri") BETWEEN 1 AND 4096)
248
+ ),
249
+ CONSTRAINT "knowledge_sources_generations_chk" CHECK (
250
+ ("current_acl_generation" IS NULL OR "current_acl_generation" > 0)
251
+ AND "sync_generation" >= 0
252
+ AND "lifecycle_generation" > 0
253
+ AND "lifecycle_state" IN ('active', 'deleted', 'revoked')
254
+ ),
255
+ CONSTRAINT "knowledge_sources_idempotency_chk" CHECK (
256
+ length(btrim("operation_id")) BETWEEN 1 AND 256
257
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
258
+ AND ("sync_cursor" IS NULL OR length("sync_cursor") <= 4096)
259
+ ),
260
+ CONSTRAINT "knowledge_sources_actor_chk" CHECK (
261
+ opengeni_private.scoped_knowledge_actor_valid(
262
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
263
+ )
264
+ ),
265
+ CONSTRAINT "knowledge_sources_external_identity_uq"
266
+ UNIQUE ("provider_id", "external_source_id"),
267
+ CONSTRAINT "knowledge_sources_operation_uq" UNIQUE ("account_id", "operation_id"),
268
+ CONSTRAINT "knowledge_sources_scope_identity_uq"
269
+ UNIQUE ("account_id", "id", "scope_key"),
270
+ CONSTRAINT "knowledge_sources_current_acl_identity_uq"
271
+ UNIQUE ("account_id", "id", "current_acl_generation")
272
+ );
273
+
274
+ CREATE INDEX "knowledge_sources_provider_idx"
275
+ ON "knowledge_sources" ("provider_id", "external_source_id");
276
+
277
+ CREATE TABLE "knowledge_source_acl_versions" (
278
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
279
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
280
+ "scope_kind" text NOT NULL,
281
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
282
+ "scope_subject_id" text,
283
+ "scope_key" text NOT NULL,
284
+ "source_id" uuid NOT NULL,
285
+ "source_scope_key" text NOT NULL,
286
+ "generation" bigint NOT NULL,
287
+ "acl_version" text,
288
+ "acl_hash" text NOT NULL,
289
+ "agent_access" boolean NOT NULL DEFAULT true,
290
+ "operation_id" text NOT NULL,
291
+ "input_hash" text NOT NULL,
292
+ "actor_kind" text NOT NULL,
293
+ "actor_subject_id" text NOT NULL,
294
+ "initiating_human_subject_id" text,
295
+ "created_at" timestamptz NOT NULL DEFAULT now(),
296
+ CONSTRAINT "knowledge_source_acl_versions_source_fk"
297
+ FOREIGN KEY ("account_id", "source_id", "source_scope_key")
298
+ REFERENCES "knowledge_sources"("account_id", "id", "scope_key") ON DELETE RESTRICT,
299
+ CONSTRAINT "knowledge_source_acl_versions_scope_chk" CHECK (
300
+ opengeni_private.scoped_knowledge_scope_valid(
301
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
302
+ )
303
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
304
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
305
+ )
306
+ ),
307
+ CONSTRAINT "knowledge_source_acl_versions_generation_chk" CHECK ("generation" > 0),
308
+ CONSTRAINT "knowledge_source_acl_versions_hash_chk" CHECK (
309
+ "acl_hash" ~ '^[0-9a-f]{64}$'
310
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
311
+ ),
312
+ CONSTRAINT "knowledge_source_acl_versions_bounds_chk" CHECK (
313
+ ("acl_version" IS NULL OR length("acl_version") BETWEEN 1 AND 512)
314
+ AND length(btrim("operation_id")) BETWEEN 1 AND 256
315
+ ),
316
+ CONSTRAINT "knowledge_source_acl_versions_actor_chk" CHECK (
317
+ opengeni_private.scoped_knowledge_actor_valid(
318
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
319
+ )
320
+ ),
321
+ CONSTRAINT "knowledge_source_acl_versions_source_generation_uq"
322
+ UNIQUE ("account_id", "source_id", "generation"),
323
+ CONSTRAINT "knowledge_source_acl_versions_source_operation_uq"
324
+ UNIQUE ("source_id", "operation_id")
325
+ );
326
+
327
+ CREATE INDEX "knowledge_source_acl_versions_source_timeline_idx"
328
+ ON "knowledge_source_acl_versions" ("source_id", "generation" DESC);
329
+
330
+ ALTER TABLE "knowledge_sources"
331
+ ADD CONSTRAINT "knowledge_sources_current_acl_fk"
332
+ FOREIGN KEY ("account_id", "id", "current_acl_generation")
333
+ REFERENCES "knowledge_source_acl_versions"("account_id", "source_id", "generation")
334
+ ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
335
+
336
+ CREATE TABLE "knowledge_sync_runs" (
337
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
338
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
339
+ "scope_kind" text NOT NULL,
340
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
341
+ "scope_subject_id" text,
342
+ "scope_key" text NOT NULL,
343
+ "source_id" uuid NOT NULL,
344
+ "input_sync_generation" bigint NOT NULL,
345
+ "input_lifecycle_generation" bigint NOT NULL,
346
+ "input_cursor" text,
347
+ "input_hash" text NOT NULL,
348
+ "operation_id" text NOT NULL,
349
+ "state" text NOT NULL DEFAULT 'started',
350
+ "output_cursor" text,
351
+ "watermark" timestamptz,
352
+ "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
353
+ "error_code" text,
354
+ "completion_hash" text,
355
+ "actor_kind" text NOT NULL,
356
+ "actor_subject_id" text NOT NULL,
357
+ "initiating_human_subject_id" text,
358
+ "started_at" timestamptz NOT NULL DEFAULT now(),
359
+ "completed_at" timestamptz,
360
+ CONSTRAINT "knowledge_sync_runs_source_fk"
361
+ FOREIGN KEY ("account_id", "source_id", "scope_key")
362
+ REFERENCES "knowledge_sources"("account_id", "id", "scope_key") ON DELETE RESTRICT,
363
+ CONSTRAINT "knowledge_sync_runs_scope_chk" CHECK (
364
+ opengeni_private.scoped_knowledge_scope_valid(
365
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
366
+ )
367
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
368
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
369
+ )
370
+ ),
371
+ CONSTRAINT "knowledge_sync_runs_input_chk" CHECK (
372
+ "input_sync_generation" >= 0
373
+ AND "input_lifecycle_generation" > 0
374
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
375
+ AND length(btrim("operation_id")) BETWEEN 1 AND 256
376
+ AND ("input_cursor" IS NULL OR length("input_cursor") <= 4096)
377
+ ),
378
+ CONSTRAINT "knowledge_sync_runs_state_chk" CHECK (
379
+ (
380
+ "state" = 'started'
381
+ AND "completed_at" IS NULL
382
+ AND "completion_hash" IS NULL
383
+ AND "output_cursor" IS NULL
384
+ AND "error_code" IS NULL
385
+ ) OR (
386
+ "state" = 'succeeded'
387
+ AND "completed_at" IS NOT NULL
388
+ AND "completion_hash" ~ '^[0-9a-f]{64}$'
389
+ AND "error_code" IS NULL
390
+ ) OR (
391
+ "state" = 'failed'
392
+ AND "completed_at" IS NOT NULL
393
+ AND "completion_hash" ~ '^[0-9a-f]{64}$'
394
+ AND length(btrim("error_code")) BETWEEN 1 AND 128
395
+ AND "output_cursor" IS NULL
396
+ )
397
+ ),
398
+ CONSTRAINT "knowledge_sync_runs_metadata_chk" CHECK (
399
+ jsonb_typeof("metadata") = 'object'
400
+ AND octet_length(convert_to("metadata"::text, 'UTF8')) <= 16384
401
+ AND ("output_cursor" IS NULL OR length("output_cursor") <= 4096)
402
+ ),
403
+ CONSTRAINT "knowledge_sync_runs_actor_chk" CHECK (
404
+ opengeni_private.scoped_knowledge_actor_valid(
405
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
406
+ )
407
+ ),
408
+ CONSTRAINT "knowledge_sync_runs_source_operation_uq" UNIQUE ("source_id", "operation_id")
409
+ );
410
+
411
+ CREATE INDEX "knowledge_sync_runs_source_state_idx"
412
+ ON "knowledge_sync_runs" ("source_id", "state", "started_at" DESC);
413
+
414
+ CREATE TABLE "knowledge_source_objects" (
415
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
416
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
417
+ "scope_kind" text NOT NULL,
418
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
419
+ "scope_subject_id" text,
420
+ "scope_key" text NOT NULL,
421
+ "source_id" uuid NOT NULL,
422
+ "external_object_id" text NOT NULL,
423
+ "document_id" uuid,
424
+ "lifecycle_state" text NOT NULL DEFAULT 'active',
425
+ "lifecycle_generation" bigint NOT NULL DEFAULT 1,
426
+ "version_generation" bigint NOT NULL DEFAULT 0,
427
+ "current_version_id" uuid,
428
+ "operation_id" text NOT NULL,
429
+ "input_hash" text NOT NULL,
430
+ "actor_kind" text NOT NULL,
431
+ "actor_subject_id" text NOT NULL,
432
+ "initiating_human_subject_id" text,
433
+ "created_at" timestamptz NOT NULL DEFAULT now(),
434
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
435
+ CONSTRAINT "knowledge_source_objects_source_fk"
436
+ FOREIGN KEY ("account_id", "source_id", "scope_key")
437
+ REFERENCES "knowledge_sources"("account_id", "id", "scope_key") ON DELETE RESTRICT,
438
+ CONSTRAINT "knowledge_source_objects_scope_chk" CHECK (
439
+ opengeni_private.scoped_knowledge_scope_valid(
440
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
441
+ )
442
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
443
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
444
+ )
445
+ ),
446
+ CONSTRAINT "knowledge_source_objects_identity_chk" CHECK (
447
+ length(btrim("external_object_id")) BETWEEN 1 AND 1024
448
+ ),
449
+ CONSTRAINT "knowledge_source_objects_generation_chk" CHECK (
450
+ "lifecycle_state" IN ('active', 'deleted', 'revoked')
451
+ AND "lifecycle_generation" > 0
452
+ AND "version_generation" >= 0
453
+ AND (("current_version_id" IS NULL) = ("version_generation" = 0))
454
+ ),
455
+ CONSTRAINT "knowledge_source_objects_idempotency_chk" CHECK (
456
+ length(btrim("operation_id")) BETWEEN 1 AND 256
457
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
458
+ ),
459
+ CONSTRAINT "knowledge_source_objects_actor_chk" CHECK (
460
+ opengeni_private.scoped_knowledge_actor_valid(
461
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
462
+ )
463
+ ),
464
+ CONSTRAINT "knowledge_source_objects_external_identity_uq"
465
+ UNIQUE ("source_id", "external_object_id"),
466
+ CONSTRAINT "knowledge_source_objects_source_operation_uq"
467
+ UNIQUE ("source_id", "operation_id"),
468
+ CONSTRAINT "knowledge_source_objects_scope_identity_uq"
469
+ UNIQUE ("account_id", "id", "scope_key"),
470
+ CONSTRAINT "knowledge_source_objects_source_identity_uq"
471
+ UNIQUE ("account_id", "id", "source_id", "scope_key"),
472
+ CONSTRAINT "knowledge_source_objects_current_version_identity_uq"
473
+ UNIQUE ("account_id", "id", "current_version_id")
474
+ );
475
+
476
+ CREATE TABLE "knowledge_document_versions" (
477
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
478
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
479
+ "scope_kind" text NOT NULL,
480
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
481
+ "scope_subject_id" text,
482
+ "scope_key" text NOT NULL,
483
+ "source_id" uuid NOT NULL,
484
+ "object_id" uuid NOT NULL,
485
+ "version_generation" bigint NOT NULL,
486
+ "external_version_id" text NOT NULL,
487
+ "content_sha256" text NOT NULL,
488
+ "ingestion_key" text NOT NULL,
489
+ "source_cursor" text,
490
+ "source_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
491
+ "source_created_at" timestamptz,
492
+ "source_updated_at" timestamptz,
493
+ "observed_at" timestamptz NOT NULL DEFAULT now(),
494
+ "acl_version_id" uuid NOT NULL,
495
+ "acl_generation" bigint NOT NULL,
496
+ "document_id" uuid,
497
+ "file_id" uuid,
498
+ "location_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
499
+ "operation_id" text NOT NULL,
500
+ "input_hash" text NOT NULL,
501
+ "actor_kind" text NOT NULL,
502
+ "actor_subject_id" text NOT NULL,
503
+ "initiating_human_subject_id" text,
504
+ "created_at" timestamptz NOT NULL DEFAULT now(),
505
+ CONSTRAINT "knowledge_document_versions_source_fk"
506
+ FOREIGN KEY ("account_id", "source_id", "scope_key")
507
+ REFERENCES "knowledge_sources"("account_id", "id", "scope_key") ON DELETE RESTRICT,
508
+ CONSTRAINT "knowledge_document_versions_object_fk"
509
+ FOREIGN KEY ("account_id", "object_id", "source_id", "scope_key")
510
+ REFERENCES "knowledge_source_objects"("account_id", "id", "source_id", "scope_key")
511
+ ON DELETE RESTRICT,
512
+ CONSTRAINT "knowledge_document_versions_acl_fk"
513
+ FOREIGN KEY ("account_id", "source_id", "acl_generation")
514
+ REFERENCES "knowledge_source_acl_versions"("account_id", "source_id", "generation")
515
+ ON DELETE RESTRICT,
516
+ CONSTRAINT "knowledge_document_versions_scope_chk" CHECK (
517
+ opengeni_private.scoped_knowledge_scope_valid(
518
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
519
+ )
520
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
521
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
522
+ )
523
+ ),
524
+ CONSTRAINT "knowledge_document_versions_identity_chk" CHECK (
525
+ "version_generation" > 0
526
+ AND length(btrim("external_version_id")) BETWEEN 1 AND 1024
527
+ AND length(btrim("ingestion_key")) BETWEEN 1 AND 512
528
+ AND "content_sha256" ~ '^[0-9a-f]{64}$'
529
+ AND "acl_generation" > 0
530
+ AND ("source_cursor" IS NULL OR length("source_cursor") <= 4096)
531
+ ),
532
+ CONSTRAINT "knowledge_document_versions_metadata_chk" CHECK (
533
+ jsonb_typeof("source_metadata") = 'object'
534
+ AND jsonb_typeof("location_metadata") = 'object'
535
+ AND octet_length(convert_to("source_metadata"::text, 'UTF8')) <= 16384
536
+ AND octet_length(convert_to("location_metadata"::text, 'UTF8')) <= 16384
537
+ ),
538
+ CONSTRAINT "knowledge_document_versions_idempotency_chk" CHECK (
539
+ length(btrim("operation_id")) BETWEEN 1 AND 256
540
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
541
+ ),
542
+ CONSTRAINT "knowledge_document_versions_actor_chk" CHECK (
543
+ opengeni_private.scoped_knowledge_actor_valid(
544
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
545
+ )
546
+ ),
547
+ CONSTRAINT "knowledge_document_versions_object_generation_uq"
548
+ UNIQUE ("account_id", "object_id", "version_generation"),
549
+ CONSTRAINT "knowledge_document_versions_object_external_version_uq"
550
+ UNIQUE ("object_id", "external_version_id"),
551
+ CONSTRAINT "knowledge_document_versions_object_ingestion_uq"
552
+ UNIQUE ("object_id", "ingestion_key"),
553
+ CONSTRAINT "knowledge_document_versions_object_operation_uq"
554
+ UNIQUE ("object_id", "operation_id"),
555
+ CONSTRAINT "knowledge_document_versions_object_identity_uq"
556
+ UNIQUE ("account_id", "object_id", "id"),
557
+ CONSTRAINT "knowledge_document_versions_scope_identity_uq"
558
+ UNIQUE ("account_id", "id", "scope_key")
559
+ );
560
+
561
+ ALTER TABLE "knowledge_source_objects"
562
+ ADD CONSTRAINT "knowledge_source_objects_current_version_fk"
563
+ FOREIGN KEY ("account_id", "id", "current_version_id")
564
+ REFERENCES "knowledge_document_versions"("account_id", "object_id", "id")
565
+ ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
566
+
567
+ CREATE TABLE "knowledge_lifecycle_events" (
568
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
569
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
570
+ "scope_kind" text NOT NULL,
571
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
572
+ "scope_subject_id" text,
573
+ "scope_key" text NOT NULL,
574
+ "target_kind" text NOT NULL,
575
+ "provider_id" uuid,
576
+ "source_id" uuid,
577
+ "object_id" uuid,
578
+ "event_type" text NOT NULL,
579
+ "old_state" text NOT NULL,
580
+ "new_state" text NOT NULL,
581
+ "old_generation" bigint NOT NULL,
582
+ "new_generation" bigint NOT NULL,
583
+ "operation_id" text NOT NULL,
584
+ "input_hash" text NOT NULL,
585
+ "reason_code" text NOT NULL,
586
+ "actor_kind" text NOT NULL,
587
+ "actor_subject_id" text NOT NULL,
588
+ "initiating_human_subject_id" text,
589
+ "created_at" timestamptz NOT NULL DEFAULT now(),
590
+ CONSTRAINT "knowledge_lifecycle_events_provider_fk"
591
+ FOREIGN KEY ("account_id", "provider_id", "scope_key")
592
+ REFERENCES "knowledge_providers"("account_id", "id", "scope_key") ON DELETE RESTRICT,
593
+ CONSTRAINT "knowledge_lifecycle_events_source_fk"
594
+ FOREIGN KEY ("account_id", "source_id", "scope_key")
595
+ REFERENCES "knowledge_sources"("account_id", "id", "scope_key") ON DELETE RESTRICT,
596
+ CONSTRAINT "knowledge_lifecycle_events_object_fk"
597
+ FOREIGN KEY ("account_id", "object_id", "scope_key")
598
+ REFERENCES "knowledge_source_objects"("account_id", "id", "scope_key") ON DELETE RESTRICT,
599
+ CONSTRAINT "knowledge_lifecycle_events_scope_chk" CHECK (
600
+ opengeni_private.scoped_knowledge_scope_valid(
601
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
602
+ )
603
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
604
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
605
+ )
606
+ ),
607
+ CONSTRAINT "knowledge_lifecycle_events_target_chk" CHECK (
608
+ ("target_kind" = 'provider' AND "provider_id" IS NOT NULL
609
+ AND "source_id" IS NULL AND "object_id" IS NULL)
610
+ OR ("target_kind" = 'source' AND "provider_id" IS NULL
611
+ AND "source_id" IS NOT NULL AND "object_id" IS NULL)
612
+ OR ("target_kind" = 'object' AND "provider_id" IS NULL
613
+ AND "source_id" IS NULL AND "object_id" IS NOT NULL)
614
+ ),
615
+ CONSTRAINT "knowledge_lifecycle_events_transition_chk" CHECK (
616
+ "event_type" IN (
617
+ 'deleted', 'revoked', 'restored', 'acl_changed',
618
+ 'sync_succeeded', 'sync_failed', 'object_version_added'
619
+ )
620
+ AND "old_state" IN ('active', 'deleted', 'revoked')
621
+ AND "new_state" IN ('active', 'deleted', 'revoked')
622
+ AND "old_generation" > 0
623
+ AND "new_generation" >= "old_generation"
624
+ AND (
625
+ ("event_type" IN ('deleted', 'revoked', 'restored')
626
+ AND "new_generation" = "old_generation" + 1)
627
+ OR ("event_type" NOT IN ('deleted', 'revoked', 'restored')
628
+ AND "new_generation" = "old_generation"
629
+ AND "new_state" = "old_state")
630
+ )
631
+ ),
632
+ CONSTRAINT "knowledge_lifecycle_events_audit_chk" CHECK (
633
+ length(btrim("operation_id")) BETWEEN 1 AND 256
634
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
635
+ AND length(btrim("reason_code")) BETWEEN 1 AND 128
636
+ AND opengeni_private.scoped_knowledge_actor_valid(
637
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
638
+ )
639
+ )
640
+ );
641
+
642
+ CREATE UNIQUE INDEX "knowledge_lifecycle_events_target_operation_uq"
643
+ ON "knowledge_lifecycle_events" (
644
+ "account_id", "target_kind", "provider_id", "source_id", "object_id", "operation_id"
645
+ ) NULLS NOT DISTINCT;
646
+ CREATE INDEX "knowledge_lifecycle_events_target_timeline_idx"
647
+ ON "knowledge_lifecycle_events" (
648
+ "target_kind", "provider_id", "source_id", "object_id", "created_at" DESC
649
+ );
650
+
651
+ CREATE TABLE "knowledge_entities" (
652
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
653
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
654
+ "scope_kind" text NOT NULL,
655
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
656
+ "scope_subject_id" text,
657
+ "scope_key" text NOT NULL,
658
+ "entity_type" text NOT NULL,
659
+ "normalized_key" text NOT NULL,
660
+ "display_name" text NOT NULL,
661
+ "operation_id" text NOT NULL,
662
+ "input_hash" text NOT NULL,
663
+ "actor_kind" text NOT NULL,
664
+ "actor_subject_id" text NOT NULL,
665
+ "initiating_human_subject_id" text,
666
+ "created_at" timestamptz NOT NULL DEFAULT now(),
667
+ CONSTRAINT "knowledge_entities_scope_chk" CHECK (
668
+ opengeni_private.scoped_knowledge_scope_valid(
669
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
670
+ )
671
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
672
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
673
+ )
674
+ ),
675
+ CONSTRAINT "knowledge_entities_key_chk" CHECK (
676
+ "entity_type" = lower(btrim("entity_type"))
677
+ AND length("entity_type") BETWEEN 1 AND 96
678
+ AND "entity_type" ~ '^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$'
679
+ AND "normalized_key" = lower(btrim("normalized_key"))
680
+ AND length("normalized_key") BETWEEN 1 AND 512
681
+ AND length(btrim("display_name")) BETWEEN 1 AND 512
682
+ ),
683
+ CONSTRAINT "knowledge_entities_audit_chk" CHECK (
684
+ length(btrim("operation_id")) BETWEEN 1 AND 256
685
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
686
+ AND opengeni_private.scoped_knowledge_actor_valid(
687
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
688
+ )
689
+ ),
690
+ CONSTRAINT "knowledge_entities_natural_identity_uq"
691
+ UNIQUE ("account_id", "scope_key", "entity_type", "normalized_key"),
692
+ CONSTRAINT "knowledge_entities_operation_uq" UNIQUE ("account_id", "operation_id"),
693
+ CONSTRAINT "knowledge_entities_scope_identity_uq"
694
+ UNIQUE ("account_id", "id", "scope_key"),
695
+ CONSTRAINT "knowledge_entities_scope_type_identity_uq"
696
+ UNIQUE ("account_id", "id", "scope_key", "entity_type")
697
+ );
698
+
699
+ CREATE TABLE "knowledge_entity_aliases" (
700
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
701
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
702
+ "scope_kind" text NOT NULL,
703
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
704
+ "scope_subject_id" text,
705
+ "scope_key" text NOT NULL,
706
+ "entity_id" uuid NOT NULL,
707
+ "entity_type" text NOT NULL,
708
+ "alias" text NOT NULL,
709
+ "normalized_alias" text NOT NULL,
710
+ "operation_id" text NOT NULL,
711
+ "input_hash" text NOT NULL,
712
+ "actor_kind" text NOT NULL,
713
+ "actor_subject_id" text NOT NULL,
714
+ "initiating_human_subject_id" text,
715
+ "created_at" timestamptz NOT NULL DEFAULT now(),
716
+ CONSTRAINT "knowledge_entity_aliases_entity_fk"
717
+ FOREIGN KEY ("account_id", "entity_id", "scope_key", "entity_type")
718
+ REFERENCES "knowledge_entities"("account_id", "id", "scope_key", "entity_type")
719
+ ON DELETE RESTRICT,
720
+ CONSTRAINT "knowledge_entity_aliases_scope_chk" CHECK (
721
+ opengeni_private.scoped_knowledge_scope_valid(
722
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
723
+ )
724
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
725
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
726
+ )
727
+ ),
728
+ CONSTRAINT "knowledge_entity_aliases_alias_chk" CHECK (
729
+ length(btrim("alias")) BETWEEN 1 AND 512
730
+ AND "normalized_alias" = lower(btrim("normalized_alias"))
731
+ AND length("normalized_alias") BETWEEN 1 AND 512
732
+ ),
733
+ CONSTRAINT "knowledge_entity_aliases_audit_chk" CHECK (
734
+ length(btrim("operation_id")) BETWEEN 1 AND 256
735
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
736
+ AND opengeni_private.scoped_knowledge_actor_valid(
737
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
738
+ )
739
+ ),
740
+ CONSTRAINT "knowledge_entity_aliases_natural_identity_uq"
741
+ UNIQUE ("account_id", "scope_key", "entity_type", "normalized_alias"),
742
+ CONSTRAINT "knowledge_entity_aliases_operation_uq" UNIQUE ("account_id", "operation_id")
743
+ );
744
+
745
+ CREATE INDEX "knowledge_entity_aliases_entity_idx"
746
+ ON "knowledge_entity_aliases" ("entity_id", "created_at");
747
+
748
+ CREATE TABLE "knowledge_facts" (
749
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
750
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
751
+ "scope_kind" text NOT NULL,
752
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
753
+ "scope_subject_id" text,
754
+ "scope_key" text NOT NULL,
755
+ "subject_entity_id" uuid NOT NULL,
756
+ "predicate_key" text NOT NULL,
757
+ "object_kind" text NOT NULL,
758
+ "object_entity_id" uuid,
759
+ "object_value" jsonb,
760
+ "object_hash" text NOT NULL,
761
+ "operation_id" text NOT NULL,
762
+ "input_hash" text NOT NULL,
763
+ "actor_kind" text NOT NULL,
764
+ "actor_subject_id" text NOT NULL,
765
+ "initiating_human_subject_id" text,
766
+ "created_at" timestamptz NOT NULL DEFAULT now(),
767
+ CONSTRAINT "knowledge_facts_subject_fk"
768
+ FOREIGN KEY ("account_id", "subject_entity_id", "scope_key")
769
+ REFERENCES "knowledge_entities"("account_id", "id", "scope_key") ON DELETE RESTRICT,
770
+ CONSTRAINT "knowledge_facts_object_entity_fk"
771
+ FOREIGN KEY ("account_id", "object_entity_id", "scope_key")
772
+ REFERENCES "knowledge_entities"("account_id", "id", "scope_key") ON DELETE RESTRICT,
773
+ CONSTRAINT "knowledge_facts_scope_chk" CHECK (
774
+ opengeni_private.scoped_knowledge_scope_valid(
775
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
776
+ )
777
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
778
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
779
+ )
780
+ ),
781
+ CONSTRAINT "knowledge_facts_predicate_chk" CHECK (
782
+ "predicate_key" = lower(btrim("predicate_key"))
783
+ AND length("predicate_key") BETWEEN 1 AND 128
784
+ AND "predicate_key" ~ '^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$'
785
+ ),
786
+ CONSTRAINT "knowledge_facts_object_shape_chk" CHECK (
787
+ ("object_kind" = 'entity' AND "object_entity_id" IS NOT NULL AND "object_value" IS NULL)
788
+ OR ("object_kind" IN ('text', 'number', 'boolean', 'json', 'timestamp')
789
+ AND "object_entity_id" IS NULL AND "object_value" IS NOT NULL)
790
+ ),
791
+ CONSTRAINT "knowledge_facts_hash_chk" CHECK (
792
+ "object_hash" ~ '^[0-9a-f]{64}$'
793
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
794
+ ),
795
+ CONSTRAINT "knowledge_facts_audit_chk" CHECK (
796
+ length(btrim("operation_id")) BETWEEN 1 AND 256
797
+ AND opengeni_private.scoped_knowledge_actor_valid(
798
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
799
+ )
800
+ ),
801
+ CONSTRAINT "knowledge_facts_natural_identity_uq"
802
+ UNIQUE ("account_id", "scope_key", "subject_entity_id", "predicate_key", "object_hash"),
803
+ CONSTRAINT "knowledge_facts_operation_uq" UNIQUE ("account_id", "operation_id"),
804
+ CONSTRAINT "knowledge_facts_scope_identity_uq"
805
+ UNIQUE ("account_id", "id", "scope_key")
806
+ );
807
+
808
+ CREATE TABLE "knowledge_claims" (
809
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
810
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
811
+ "scope_kind" text NOT NULL,
812
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
813
+ "scope_subject_id" text,
814
+ "scope_key" text NOT NULL,
815
+ "fact_id" uuid NOT NULL,
816
+ "origin" text NOT NULL,
817
+ "confidence_bps" integer NOT NULL,
818
+ "effective_at" timestamptz NOT NULL,
819
+ "expires_at" timestamptz,
820
+ "extraction_method" text NOT NULL,
821
+ "extraction_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
822
+ "model_provider" text,
823
+ "model_name" text,
824
+ "model_version" text,
825
+ "operation_id" text NOT NULL,
826
+ "input_hash" text NOT NULL,
827
+ "actor_kind" text NOT NULL,
828
+ "actor_subject_id" text NOT NULL,
829
+ "initiating_human_subject_id" text,
830
+ "created_at" timestamptz NOT NULL DEFAULT now(),
831
+ CONSTRAINT "knowledge_claims_fact_fk"
832
+ FOREIGN KEY ("account_id", "fact_id", "scope_key")
833
+ REFERENCES "knowledge_facts"("account_id", "id", "scope_key") ON DELETE RESTRICT,
834
+ CONSTRAINT "knowledge_claims_scope_chk" CHECK (
835
+ opengeni_private.scoped_knowledge_scope_valid(
836
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
837
+ )
838
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
839
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
840
+ )
841
+ ),
842
+ CONSTRAINT "knowledge_claims_assertion_chk" CHECK (
843
+ "origin" IN ('explicit', 'inferred')
844
+ AND "confidence_bps" BETWEEN 0 AND 10000
845
+ AND ("expires_at" IS NULL OR "effective_at" < "expires_at")
846
+ AND length(btrim("extraction_method")) BETWEEN 1 AND 128
847
+ ),
848
+ CONSTRAINT "knowledge_claims_metadata_chk" CHECK (
849
+ jsonb_typeof("extraction_metadata") = 'object'
850
+ AND octet_length(convert_to("extraction_metadata"::text, 'UTF8')) <= 16384
851
+ AND ("model_provider" IS NULL OR length("model_provider") BETWEEN 1 AND 128)
852
+ AND ("model_name" IS NULL OR length("model_name") BETWEEN 1 AND 256)
853
+ AND ("model_version" IS NULL OR length("model_version") BETWEEN 1 AND 256)
854
+ ),
855
+ CONSTRAINT "knowledge_claims_audit_chk" CHECK (
856
+ length(btrim("operation_id")) BETWEEN 1 AND 256
857
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
858
+ AND opengeni_private.scoped_knowledge_actor_valid(
859
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
860
+ )
861
+ ),
862
+ CONSTRAINT "knowledge_claims_operation_uq" UNIQUE ("account_id", "operation_id"),
863
+ CONSTRAINT "knowledge_claims_scope_identity_uq"
864
+ UNIQUE ("account_id", "id", "scope_key")
865
+ );
866
+
867
+ CREATE INDEX "knowledge_claims_fact_timeline_idx"
868
+ ON "knowledge_claims" ("fact_id", "effective_at" DESC, "created_at" DESC);
869
+
870
+ CREATE TABLE "knowledge_claim_relations" (
871
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
872
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
873
+ "scope_kind" text NOT NULL,
874
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
875
+ "scope_subject_id" text,
876
+ "scope_key" text NOT NULL,
877
+ "relation_type" text NOT NULL,
878
+ "from_claim_id" uuid NOT NULL,
879
+ "to_claim_id" uuid NOT NULL,
880
+ "operation_id" text NOT NULL,
881
+ "input_hash" text NOT NULL,
882
+ "actor_kind" text NOT NULL,
883
+ "actor_subject_id" text NOT NULL,
884
+ "initiating_human_subject_id" text,
885
+ "created_at" timestamptz NOT NULL DEFAULT now(),
886
+ CONSTRAINT "knowledge_claim_relations_from_fk"
887
+ FOREIGN KEY ("account_id", "from_claim_id", "scope_key")
888
+ REFERENCES "knowledge_claims"("account_id", "id", "scope_key") ON DELETE RESTRICT,
889
+ CONSTRAINT "knowledge_claim_relations_to_fk"
890
+ FOREIGN KEY ("account_id", "to_claim_id", "scope_key")
891
+ REFERENCES "knowledge_claims"("account_id", "id", "scope_key") ON DELETE RESTRICT,
892
+ CONSTRAINT "knowledge_claim_relations_scope_chk" CHECK (
893
+ opengeni_private.scoped_knowledge_scope_valid(
894
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
895
+ )
896
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
897
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
898
+ )
899
+ ),
900
+ CONSTRAINT "knowledge_claim_relations_shape_chk" CHECK (
901
+ "relation_type" IN ('supersedes', 'conflicts_with')
902
+ AND "from_claim_id" <> "to_claim_id"
903
+ AND (
904
+ "relation_type" <> 'conflicts_with'
905
+ OR "from_claim_id"::text < "to_claim_id"::text
906
+ )
907
+ ),
908
+ CONSTRAINT "knowledge_claim_relations_audit_chk" CHECK (
909
+ length(btrim("operation_id")) BETWEEN 1 AND 256
910
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
911
+ AND opengeni_private.scoped_knowledge_actor_valid(
912
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
913
+ )
914
+ ),
915
+ CONSTRAINT "knowledge_claim_relations_natural_identity_uq"
916
+ UNIQUE ("relation_type", "from_claim_id", "to_claim_id"),
917
+ CONSTRAINT "knowledge_claim_relations_operation_uq" UNIQUE ("account_id", "operation_id")
918
+ );
919
+
920
+ CREATE TABLE "knowledge_claim_evidence" (
921
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
922
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
923
+ "scope_kind" text NOT NULL,
924
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
925
+ "scope_subject_id" text,
926
+ "scope_key" text NOT NULL,
927
+ "claim_id" uuid NOT NULL,
928
+ "document_version_id" uuid NOT NULL,
929
+ "polarity" text NOT NULL,
930
+ "document_chunk_id" uuid,
931
+ "chunk_index" integer,
932
+ "locator" text,
933
+ "quote_hash" text,
934
+ "content_hash" text NOT NULL,
935
+ "operation_id" text NOT NULL,
936
+ "input_hash" text NOT NULL,
937
+ "actor_kind" text NOT NULL,
938
+ "actor_subject_id" text NOT NULL,
939
+ "initiating_human_subject_id" text,
940
+ "created_at" timestamptz NOT NULL DEFAULT now(),
941
+ CONSTRAINT "knowledge_claim_evidence_claim_fk"
942
+ FOREIGN KEY ("account_id", "claim_id", "scope_key")
943
+ REFERENCES "knowledge_claims"("account_id", "id", "scope_key") ON DELETE RESTRICT,
944
+ CONSTRAINT "knowledge_claim_evidence_version_fk"
945
+ FOREIGN KEY ("account_id", "document_version_id", "scope_key")
946
+ REFERENCES "knowledge_document_versions"("account_id", "id", "scope_key") ON DELETE RESTRICT,
947
+ CONSTRAINT "knowledge_claim_evidence_scope_chk" CHECK (
948
+ opengeni_private.scoped_knowledge_scope_valid(
949
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
950
+ )
951
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
952
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
953
+ )
954
+ ),
955
+ CONSTRAINT "knowledge_claim_evidence_location_chk" CHECK (
956
+ "polarity" IN ('supports', 'contradicts')
957
+ AND ("chunk_index" IS NULL OR "chunk_index" >= 0)
958
+ AND ("locator" IS NULL OR length("locator") BETWEEN 1 AND 2048)
959
+ AND ("quote_hash" IS NULL OR "quote_hash" ~ '^[0-9a-f]{64}$')
960
+ AND "content_hash" ~ '^[0-9a-f]{64}$'
961
+ ),
962
+ CONSTRAINT "knowledge_claim_evidence_audit_chk" CHECK (
963
+ length(btrim("operation_id")) BETWEEN 1 AND 256
964
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
965
+ AND opengeni_private.scoped_knowledge_actor_valid(
966
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
967
+ )
968
+ ),
969
+ CONSTRAINT "knowledge_claim_evidence_operation_uq" UNIQUE ("account_id", "operation_id"),
970
+ CONSTRAINT "knowledge_claim_evidence_scope_identity_uq"
971
+ UNIQUE ("account_id", "id", "scope_key"),
972
+ CONSTRAINT "knowledge_claim_evidence_claim_identity_uq"
973
+ UNIQUE ("account_id", "id", "claim_id", "scope_key")
974
+ );
975
+
976
+ CREATE UNIQUE INDEX "knowledge_claim_evidence_natural_identity_uq"
977
+ ON "knowledge_claim_evidence" (
978
+ "claim_id", "document_version_id", "polarity",
979
+ coalesce("document_chunk_id"::text, ''), coalesce("locator", '')
980
+ );
981
+ CREATE INDEX "knowledge_claim_evidence_claim_polarity_idx"
982
+ ON "knowledge_claim_evidence" ("claim_id", "polarity", "created_at");
983
+
984
+ CREATE TABLE "knowledge_claim_reviews" (
985
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
986
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
987
+ "scope_kind" text NOT NULL,
988
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
989
+ "scope_subject_id" text,
990
+ "scope_key" text NOT NULL,
991
+ "claim_id" uuid NOT NULL,
992
+ "review_revision" bigint NOT NULL DEFAULT nextval('knowledge_claim_review_revision_seq'),
993
+ "state" text NOT NULL,
994
+ "reason" text NOT NULL,
995
+ "operation_id" text NOT NULL,
996
+ "input_hash" text NOT NULL,
997
+ "actor_kind" text NOT NULL,
998
+ "actor_subject_id" text NOT NULL,
999
+ "initiating_human_subject_id" text,
1000
+ "created_at" timestamptz NOT NULL DEFAULT now(),
1001
+ CONSTRAINT "knowledge_claim_reviews_claim_fk"
1002
+ FOREIGN KEY ("account_id", "claim_id", "scope_key")
1003
+ REFERENCES "knowledge_claims"("account_id", "id", "scope_key") ON DELETE RESTRICT,
1004
+ CONSTRAINT "knowledge_claim_reviews_scope_chk" CHECK (
1005
+ opengeni_private.scoped_knowledge_scope_valid(
1006
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
1007
+ )
1008
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
1009
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
1010
+ )
1011
+ ),
1012
+ CONSTRAINT "knowledge_claim_reviews_review_chk" CHECK (
1013
+ "state" IN ('proposed', 'approved', 'rejected', 'revoked')
1014
+ AND "review_revision" > 0
1015
+ AND length(btrim("reason")) BETWEEN 1 AND 4096
1016
+ AND (
1017
+ "state" = 'proposed'
1018
+ OR ("actor_kind" = 'human' AND "actor_subject_id" = "initiating_human_subject_id")
1019
+ )
1020
+ ),
1021
+ CONSTRAINT "knowledge_claim_reviews_audit_chk" CHECK (
1022
+ length(btrim("operation_id")) BETWEEN 1 AND 256
1023
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
1024
+ AND opengeni_private.scoped_knowledge_actor_valid(
1025
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
1026
+ )
1027
+ ),
1028
+ CONSTRAINT "knowledge_claim_reviews_claim_revision_uq"
1029
+ UNIQUE ("claim_id", "review_revision"),
1030
+ CONSTRAINT "knowledge_claim_reviews_operation_uq" UNIQUE ("account_id", "operation_id")
1031
+ );
1032
+
1033
+ CREATE INDEX "knowledge_claim_reviews_claim_timeline_idx"
1034
+ ON "knowledge_claim_reviews" ("claim_id", "review_revision" DESC);
1035
+
1036
+ CREATE TABLE "knowledge_change_proposals" (
1037
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1038
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE RESTRICT,
1039
+ "scope_kind" text NOT NULL,
1040
+ "scope_workspace_id" uuid REFERENCES "workspaces"("id") ON DELETE RESTRICT,
1041
+ "scope_subject_id" text,
1042
+ "scope_key" text NOT NULL,
1043
+ "target_kind" text NOT NULL,
1044
+ "target_scope" text NOT NULL,
1045
+ "target_key" text,
1046
+ "content" text NOT NULL,
1047
+ "content_hash" text NOT NULL,
1048
+ "claim_id" uuid NOT NULL,
1049
+ "evidence_id" uuid NOT NULL,
1050
+ "status" text NOT NULL DEFAULT 'proposed',
1051
+ "operation_id" text NOT NULL,
1052
+ "input_hash" text NOT NULL,
1053
+ "actor_kind" text NOT NULL,
1054
+ "actor_subject_id" text NOT NULL,
1055
+ "initiating_human_subject_id" text,
1056
+ "created_at" timestamptz NOT NULL DEFAULT now(),
1057
+ CONSTRAINT "knowledge_change_proposals_claim_fk"
1058
+ FOREIGN KEY ("account_id", "claim_id", "scope_key")
1059
+ REFERENCES "knowledge_claims"("account_id", "id", "scope_key") ON DELETE RESTRICT,
1060
+ CONSTRAINT "knowledge_change_proposals_evidence_fk"
1061
+ FOREIGN KEY ("account_id", "evidence_id", "claim_id", "scope_key")
1062
+ REFERENCES "knowledge_claim_evidence"("account_id", "id", "claim_id", "scope_key")
1063
+ ON DELETE RESTRICT,
1064
+ CONSTRAINT "knowledge_change_proposals_scope_chk" CHECK (
1065
+ opengeni_private.scoped_knowledge_scope_valid(
1066
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
1067
+ )
1068
+ AND "scope_key" = opengeni_private.scoped_knowledge_scope_key(
1069
+ "scope_kind", "scope_workspace_id", "scope_subject_id"
1070
+ )
1071
+ ),
1072
+ CONSTRAINT "knowledge_change_proposals_target_chk" CHECK (
1073
+ (
1074
+ "target_kind" = 'instruction_policy'
1075
+ AND (
1076
+ ("target_scope" = 'global' AND "target_key" IS NULL)
1077
+ OR ("target_scope" = 'role' AND length(btrim("target_key")) BETWEEN 1 AND 96)
1078
+ )
1079
+ ) OR (
1080
+ "target_kind" = 'preference'
1081
+ AND "target_scope" IN ('organization', 'workspace', 'personal')
1082
+ AND "target_key" = lower(btrim("target_key"))
1083
+ AND length("target_key") BETWEEN 1 AND 96
1084
+ AND "target_key" ~ '^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$'
1085
+ )
1086
+ ),
1087
+ CONSTRAINT "knowledge_change_proposals_content_chk" CHECK (
1088
+ "status" = 'proposed'
1089
+ AND length(btrim("content")) > 0
1090
+ AND octet_length("content") <= 1048576
1091
+ AND "content_hash" ~ '^[0-9a-f]{64}$'
1092
+ AND "content_hash" = encode(sha256(convert_to("content", 'UTF8')), 'hex')
1093
+ ),
1094
+ CONSTRAINT "knowledge_change_proposals_audit_chk" CHECK (
1095
+ length(btrim("operation_id")) BETWEEN 1 AND 256
1096
+ AND "input_hash" ~ '^[0-9a-f]{64}$'
1097
+ AND opengeni_private.scoped_knowledge_actor_valid(
1098
+ "actor_kind", "actor_subject_id", "initiating_human_subject_id"
1099
+ )
1100
+ ),
1101
+ CONSTRAINT "knowledge_change_proposals_operation_uq" UNIQUE ("account_id", "operation_id")
1102
+ );
1103
+
1104
+ CREATE INDEX "knowledge_change_proposals_claim_timeline_idx"
1105
+ ON "knowledge_change_proposals" ("claim_id", "created_at" DESC);
1106
+
1107
+ DO $scope_workspace_fks$
1108
+ DECLARE table_name text;
1109
+ BEGIN
1110
+ FOREACH table_name IN ARRAY ARRAY[
1111
+ 'knowledge_operation_receipts',
1112
+ 'knowledge_providers',
1113
+ 'knowledge_sources',
1114
+ 'knowledge_source_acl_versions',
1115
+ 'knowledge_sync_runs',
1116
+ 'knowledge_source_objects',
1117
+ 'knowledge_document_versions',
1118
+ 'knowledge_lifecycle_events',
1119
+ 'knowledge_entities',
1120
+ 'knowledge_entity_aliases',
1121
+ 'knowledge_facts',
1122
+ 'knowledge_claims',
1123
+ 'knowledge_claim_relations',
1124
+ 'knowledge_claim_evidence',
1125
+ 'knowledge_claim_reviews',
1126
+ 'knowledge_change_proposals'
1127
+ ] LOOP
1128
+ EXECUTE format(
1129
+ 'ALTER TABLE %I ADD CONSTRAINT %I '
1130
+ || 'FOREIGN KEY (scope_workspace_id, account_id) '
1131
+ || 'REFERENCES workspaces(id, account_id) ON DELETE RESTRICT',
1132
+ table_name,
1133
+ table_name || '_scope_workspace_account_fk'
1134
+ );
1135
+ END LOOP;
1136
+ END
1137
+ $scope_workspace_fks$;
1138
+
1139
+ CREATE OR REPLACE FUNCTION scoped_knowledge_guard_head_insert()
1140
+ RETURNS trigger LANGUAGE plpgsql AS $$
1141
+ DECLARE row_value jsonb := to_jsonb(NEW);
1142
+ BEGIN
1143
+ IF TG_TABLE_NAME = 'knowledge_providers' THEN
1144
+ IF row_value->>'lifecycle_state' <> 'active'
1145
+ OR (row_value->>'lifecycle_generation')::bigint <> 1
1146
+ THEN
1147
+ RAISE EXCEPTION 'knowledge provider must start active at generation one'
1148
+ USING ERRCODE = '23514';
1149
+ END IF;
1150
+ ELSIF TG_TABLE_NAME = 'knowledge_sources' THEN
1151
+ IF row_value->>'lifecycle_state' <> 'active'
1152
+ OR (row_value->>'lifecycle_generation')::bigint <> 1
1153
+ OR (row_value->>'sync_generation')::bigint <> 0
1154
+ OR row_value->'current_acl_generation' <> 'null'::jsonb
1155
+ OR row_value->'sync_cursor' <> 'null'::jsonb
1156
+ THEN
1157
+ RAISE EXCEPTION 'knowledge source initial lifecycle, ACL, and sync state is invalid'
1158
+ USING ERRCODE = '23514';
1159
+ END IF;
1160
+ ELSIF TG_TABLE_NAME = 'knowledge_source_objects' THEN
1161
+ IF row_value->>'lifecycle_state' <> 'active'
1162
+ OR (row_value->>'lifecycle_generation')::bigint <> 1
1163
+ OR (row_value->>'version_generation')::bigint <> 0
1164
+ OR row_value->'current_version_id' <> 'null'::jsonb
1165
+ THEN
1166
+ RAISE EXCEPTION 'knowledge source object must start active without a current version'
1167
+ USING ERRCODE = '23514';
1168
+ END IF;
1169
+ END IF;
1170
+ RETURN NEW;
1171
+ END;
1172
+ $$;
1173
+
1174
+ DO $insert_guard_functions$
1175
+ DECLARE data_schema text := current_schema();
1176
+ BEGIN
1177
+ EXECUTE format($ddl$
1178
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_guard_acl_insert()
1179
+ RETURNS trigger
1180
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
1181
+ AS $body$
1182
+ DECLARE source_row knowledge_sources%%ROWTYPE;
1183
+ BEGIN
1184
+ IF NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
1185
+ OR NOT opengeni_private.scoped_knowledge_scope_visible(
1186
+ NEW.account_id, NEW.scope_kind, NEW.scope_workspace_id, NEW.scope_subject_id
1187
+ )
1188
+ OR NOT opengeni_private.scoped_knowledge_actor_authorized(
1189
+ NEW.actor_kind, NEW.actor_subject_id, NEW.initiating_human_subject_id
1190
+ )
1191
+ THEN
1192
+ RAISE EXCEPTION 'knowledge ACL insert authority is invalid'
1193
+ USING ERRCODE = '42501';
1194
+ END IF;
1195
+ SELECT * INTO source_row
1196
+ FROM knowledge_sources source
1197
+ WHERE source.id = NEW.source_id
1198
+ AND source.account_id = NEW.account_id
1199
+ AND source.scope_key = NEW.source_scope_key
1200
+ AND opengeni_private.scoped_knowledge_scope_visible(
1201
+ source.account_id, source.scope_kind,
1202
+ source.scope_workspace_id, source.scope_subject_id
1203
+ )
1204
+ FOR UPDATE;
1205
+ IF NOT FOUND
1206
+ OR NEW.generation > coalesce(source_row.current_acl_generation, 0) + 1
1207
+ OR (
1208
+ NEW.generation = coalesce(source_row.current_acl_generation, 0) + 1
1209
+ AND source_row.lifecycle_state <> 'active'
1210
+ )
1211
+ THEN
1212
+ RAISE EXCEPTION 'knowledge ACL insert is outside the current source generation fence'
1213
+ USING ERRCODE = '40001';
1214
+ END IF;
1215
+ RETURN NEW;
1216
+ END;
1217
+ $body$;
1218
+
1219
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_guard_sync_insert()
1220
+ RETURNS trigger
1221
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
1222
+ AS $body$
1223
+ DECLARE source_row knowledge_sources%%ROWTYPE;
1224
+ BEGIN
1225
+ IF NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
1226
+ OR NOT opengeni_private.scoped_knowledge_scope_visible(
1227
+ NEW.account_id, NEW.scope_kind, NEW.scope_workspace_id, NEW.scope_subject_id
1228
+ )
1229
+ OR NOT opengeni_private.scoped_knowledge_actor_authorized(
1230
+ NEW.actor_kind, NEW.actor_subject_id, NEW.initiating_human_subject_id
1231
+ )
1232
+ THEN
1233
+ RAISE EXCEPTION 'knowledge sync insert authority is invalid'
1234
+ USING ERRCODE = '42501';
1235
+ END IF;
1236
+ SELECT * INTO source_row
1237
+ FROM knowledge_sources source
1238
+ WHERE source.id = NEW.source_id
1239
+ AND source.account_id = NEW.account_id
1240
+ AND source.scope_key = NEW.scope_key
1241
+ AND source.scope_kind = NEW.scope_kind
1242
+ AND source.scope_workspace_id IS NOT DISTINCT FROM NEW.scope_workspace_id
1243
+ AND source.scope_subject_id IS NOT DISTINCT FROM NEW.scope_subject_id
1244
+ AND opengeni_private.scoped_knowledge_scope_visible(
1245
+ source.account_id, source.scope_kind,
1246
+ source.scope_workspace_id, source.scope_subject_id
1247
+ )
1248
+ FOR UPDATE;
1249
+ IF NOT FOUND
1250
+ OR source_row.lifecycle_state <> 'active'
1251
+ OR NEW.state <> 'started'
1252
+ OR NEW.input_lifecycle_generation <> source_row.lifecycle_generation
1253
+ OR NEW.input_sync_generation <> source_row.sync_generation
1254
+ OR NEW.input_cursor IS DISTINCT FROM source_row.sync_cursor
1255
+ THEN
1256
+ RAISE EXCEPTION 'knowledge sync insert is outside the current source generation fence'
1257
+ USING ERRCODE = '40001';
1258
+ END IF;
1259
+ RETURN NEW;
1260
+ END;
1261
+ $body$;
1262
+
1263
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_guard_version_insert()
1264
+ RETURNS trigger
1265
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
1266
+ AS $body$
1267
+ DECLARE object_row knowledge_source_objects%%ROWTYPE;
1268
+ DECLARE source_row knowledge_sources%%ROWTYPE;
1269
+ BEGIN
1270
+ IF NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
1271
+ OR NOT opengeni_private.scoped_knowledge_scope_visible(
1272
+ NEW.account_id, NEW.scope_kind, NEW.scope_workspace_id, NEW.scope_subject_id
1273
+ )
1274
+ OR NOT opengeni_private.scoped_knowledge_actor_authorized(
1275
+ NEW.actor_kind, NEW.actor_subject_id, NEW.initiating_human_subject_id
1276
+ )
1277
+ THEN
1278
+ RAISE EXCEPTION 'knowledge document version insert authority is invalid'
1279
+ USING ERRCODE = '42501';
1280
+ END IF;
1281
+ SELECT * INTO object_row
1282
+ FROM knowledge_source_objects object
1283
+ WHERE object.id = NEW.object_id
1284
+ AND object.account_id = NEW.account_id
1285
+ AND object.source_id = NEW.source_id
1286
+ AND object.scope_key = NEW.scope_key
1287
+ AND object.scope_kind = NEW.scope_kind
1288
+ AND object.scope_workspace_id IS NOT DISTINCT FROM NEW.scope_workspace_id
1289
+ AND object.scope_subject_id IS NOT DISTINCT FROM NEW.scope_subject_id
1290
+ AND opengeni_private.scoped_knowledge_scope_visible(
1291
+ object.account_id, object.scope_kind,
1292
+ object.scope_workspace_id, object.scope_subject_id
1293
+ )
1294
+ FOR UPDATE;
1295
+ IF NOT FOUND THEN
1296
+ RAISE EXCEPTION 'knowledge document version object is outside the exact scope'
1297
+ USING ERRCODE = '23514';
1298
+ END IF;
1299
+ SELECT * INTO source_row
1300
+ FROM knowledge_sources source
1301
+ WHERE source.id = NEW.source_id
1302
+ AND source.account_id = NEW.account_id
1303
+ AND source.scope_key = NEW.scope_key
1304
+ AND source.scope_kind = NEW.scope_kind
1305
+ AND source.scope_workspace_id IS NOT DISTINCT FROM NEW.scope_workspace_id
1306
+ AND source.scope_subject_id IS NOT DISTINCT FROM NEW.scope_subject_id
1307
+ AND opengeni_private.scoped_knowledge_scope_visible(
1308
+ source.account_id, source.scope_kind,
1309
+ source.scope_workspace_id, source.scope_subject_id
1310
+ )
1311
+ FOR UPDATE;
1312
+ IF NOT FOUND
1313
+ OR NEW.version_generation > object_row.version_generation + 1
1314
+ OR (
1315
+ NEW.version_generation = object_row.version_generation + 1
1316
+ AND (
1317
+ object_row.lifecycle_state <> 'active'
1318
+ OR source_row.lifecycle_state <> 'active'
1319
+ OR NEW.acl_generation IS DISTINCT FROM source_row.current_acl_generation
1320
+ )
1321
+ )
1322
+ THEN
1323
+ RAISE EXCEPTION 'knowledge document version is outside the current object/source fence'
1324
+ USING ERRCODE = '40001';
1325
+ END IF;
1326
+ RETURN NEW;
1327
+ END;
1328
+ $body$;
1329
+ $ddl$, data_schema);
1330
+
1331
+ EXECUTE format(
1332
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_guard_acl_insert() FROM PUBLIC',
1333
+ data_schema
1334
+ );
1335
+ EXECUTE format(
1336
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_guard_sync_insert() FROM PUBLIC',
1337
+ data_schema
1338
+ );
1339
+ EXECUTE format(
1340
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_guard_version_insert() FROM PUBLIC',
1341
+ data_schema
1342
+ );
1343
+ END
1344
+ $insert_guard_functions$;
1345
+
1346
+ CREATE TRIGGER knowledge_providers_10_initial_guard
1347
+ BEFORE INSERT ON "knowledge_providers"
1348
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_head_insert();
1349
+ CREATE TRIGGER knowledge_sources_10_initial_guard
1350
+ BEFORE INSERT ON "knowledge_sources"
1351
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_head_insert();
1352
+ CREATE TRIGGER knowledge_source_objects_10_initial_guard
1353
+ BEFORE INSERT ON "knowledge_source_objects"
1354
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_head_insert();
1355
+ CREATE TRIGGER knowledge_source_acl_versions_10_generation_guard
1356
+ BEFORE INSERT ON "knowledge_source_acl_versions"
1357
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_acl_insert();
1358
+ CREATE TRIGGER knowledge_sync_runs_10_generation_guard
1359
+ BEFORE INSERT ON "knowledge_sync_runs"
1360
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_sync_insert();
1361
+ CREATE TRIGGER knowledge_document_versions_10_generation_guard
1362
+ BEFORE INSERT ON "knowledge_document_versions"
1363
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_version_insert();
1364
+
1365
+ CREATE OR REPLACE FUNCTION scoped_knowledge_validate_bridge()
1366
+ RETURNS trigger LANGUAGE plpgsql AS $$
1367
+ DECLARE version_document_id uuid;
1368
+ BEGIN
1369
+ IF TG_TABLE_NAME = 'knowledge_source_objects' THEN
1370
+ IF NEW.document_id IS NOT NULL THEN
1371
+ IF NEW.scope_workspace_id IS NULL OR NOT EXISTS (
1372
+ SELECT 1 FROM documents document
1373
+ WHERE document.id = NEW.document_id
1374
+ AND document.account_id = NEW.account_id
1375
+ AND document.workspace_id = NEW.scope_workspace_id
1376
+ ) THEN
1377
+ RAISE EXCEPTION 'knowledge source object document bridge is outside its exact account/workspace'
1378
+ USING ERRCODE = '23514';
1379
+ END IF;
1380
+ END IF;
1381
+ ELSIF TG_TABLE_NAME = 'knowledge_document_versions' THEN
1382
+ IF NOT EXISTS (
1383
+ SELECT 1 FROM knowledge_source_acl_versions acl
1384
+ WHERE acl.id = NEW.acl_version_id
1385
+ AND acl.account_id = NEW.account_id
1386
+ AND acl.source_id = NEW.source_id
1387
+ AND acl.generation = NEW.acl_generation
1388
+ ) THEN
1389
+ RAISE EXCEPTION 'knowledge document version ACL reference is not exact'
1390
+ USING ERRCODE = '23514';
1391
+ END IF;
1392
+ IF NEW.document_id IS NULL AND NEW.file_id IS NOT NULL THEN
1393
+ RAISE EXCEPTION 'knowledge document version file bridge requires a document bridge'
1394
+ USING ERRCODE = '23514';
1395
+ END IF;
1396
+ IF NEW.document_id IS NOT NULL AND (
1397
+ NEW.scope_workspace_id IS NULL OR NOT EXISTS (
1398
+ SELECT 1 FROM documents document
1399
+ WHERE document.id = NEW.document_id
1400
+ AND document.account_id = NEW.account_id
1401
+ AND document.workspace_id = NEW.scope_workspace_id
1402
+ AND (NEW.file_id IS NULL OR document.file_id = NEW.file_id)
1403
+ )
1404
+ ) THEN
1405
+ RAISE EXCEPTION 'knowledge document version bridge is outside its exact account/workspace/file'
1406
+ USING ERRCODE = '23514';
1407
+ END IF;
1408
+ ELSIF TG_TABLE_NAME = 'knowledge_claim_evidence' THEN
1409
+ IF NEW.document_chunk_id IS NOT NULL THEN
1410
+ SELECT document_id INTO version_document_id
1411
+ FROM knowledge_document_versions
1412
+ WHERE id = NEW.document_version_id;
1413
+ IF version_document_id IS NULL OR NEW.scope_workspace_id IS NULL OR NOT EXISTS (
1414
+ SELECT 1 FROM document_chunks chunk
1415
+ WHERE chunk.id = NEW.document_chunk_id
1416
+ AND chunk.account_id = NEW.account_id
1417
+ AND chunk.workspace_id = NEW.scope_workspace_id
1418
+ AND chunk.document_id = version_document_id
1419
+ AND (NEW.chunk_index IS NULL OR chunk.chunk_index = NEW.chunk_index)
1420
+ ) THEN
1421
+ RAISE EXCEPTION 'knowledge claim evidence chunk bridge is not exact'
1422
+ USING ERRCODE = '23514';
1423
+ END IF;
1424
+ END IF;
1425
+ END IF;
1426
+ RETURN NEW;
1427
+ END;
1428
+ $$;
1429
+
1430
+ CREATE TRIGGER knowledge_source_objects_20_validate_bridge
1431
+ BEFORE INSERT ON "knowledge_source_objects"
1432
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_validate_bridge();
1433
+ CREATE TRIGGER knowledge_document_versions_20_validate_bridge
1434
+ BEFORE INSERT ON "knowledge_document_versions"
1435
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_validate_bridge();
1436
+ CREATE TRIGGER knowledge_claim_evidence_20_validate_bridge
1437
+ BEFORE INSERT ON "knowledge_claim_evidence"
1438
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_validate_bridge();
1439
+
1440
+ CREATE OR REPLACE FUNCTION scoped_knowledge_validate_claim_relation()
1441
+ RETURNS trigger LANGUAGE plpgsql AS $$
1442
+ BEGIN
1443
+ PERFORM pg_catalog.pg_advisory_xact_lock(
1444
+ pg_catalog.hashtextextended(NEW.account_id::text || ':' || NEW.scope_key, 0)
1445
+ );
1446
+ IF NEW.relation_type = 'supersedes' AND EXISTS (
1447
+ WITH RECURSIVE path(claim_id) AS (
1448
+ SELECT relation.to_claim_id
1449
+ FROM knowledge_claim_relations relation
1450
+ WHERE relation.account_id = NEW.account_id
1451
+ AND relation.scope_key = NEW.scope_key
1452
+ AND relation.relation_type = 'supersedes'
1453
+ AND relation.from_claim_id = NEW.to_claim_id
1454
+ UNION
1455
+ SELECT relation.to_claim_id
1456
+ FROM knowledge_claim_relations relation
1457
+ JOIN path ON relation.from_claim_id = path.claim_id
1458
+ WHERE relation.account_id = NEW.account_id
1459
+ AND relation.scope_key = NEW.scope_key
1460
+ AND relation.relation_type = 'supersedes'
1461
+ )
1462
+ SELECT 1 FROM path WHERE claim_id = NEW.from_claim_id
1463
+ ) THEN
1464
+ RAISE EXCEPTION 'knowledge claim supersession cannot create a cycle'
1465
+ USING ERRCODE = '23514';
1466
+ END IF;
1467
+ RETURN NEW;
1468
+ END;
1469
+ $$;
1470
+
1471
+ CREATE TRIGGER knowledge_claim_relations_validate
1472
+ BEFORE INSERT ON "knowledge_claim_relations"
1473
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_validate_claim_relation();
1474
+
1475
+ CREATE OR REPLACE FUNCTION scoped_knowledge_reject_immutable_mutation()
1476
+ RETURNS trigger LANGUAGE plpgsql AS $$
1477
+ BEGIN
1478
+ RAISE EXCEPTION 'scoped knowledge provenance is immutable' USING ERRCODE = '55000';
1479
+ END;
1480
+ $$;
1481
+
1482
+ DO $immutable_triggers$
1483
+ DECLARE table_name text;
1484
+ BEGIN
1485
+ FOREACH table_name IN ARRAY ARRAY[
1486
+ 'knowledge_operation_receipts',
1487
+ 'knowledge_source_acl_versions',
1488
+ 'knowledge_document_versions',
1489
+ 'knowledge_lifecycle_events',
1490
+ 'knowledge_entities',
1491
+ 'knowledge_entity_aliases',
1492
+ 'knowledge_facts',
1493
+ 'knowledge_claims',
1494
+ 'knowledge_claim_relations',
1495
+ 'knowledge_claim_evidence',
1496
+ 'knowledge_claim_reviews',
1497
+ 'knowledge_change_proposals'
1498
+ ] LOOP
1499
+ EXECUTE format(
1500
+ 'CREATE TRIGGER %I BEFORE UPDATE OR DELETE ON %I '
1501
+ || 'FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_reject_immutable_mutation()',
1502
+ table_name || '_immutable', table_name
1503
+ );
1504
+ END LOOP;
1505
+ END
1506
+ $immutable_triggers$;
1507
+
1508
+ CREATE OR REPLACE FUNCTION scoped_knowledge_guard_provider_mutation()
1509
+ RETURNS trigger LANGUAGE plpgsql AS $$
1510
+ DECLARE mutation_writer boolean;
1511
+ BEGIN
1512
+ IF TG_OP = 'DELETE' THEN
1513
+ RAISE EXCEPTION 'knowledge provider tombstones are not deleted' USING ERRCODE = '55000';
1514
+ END IF;
1515
+ SELECT current_user = pg_get_userbyid(procedure_row.proowner)
1516
+ INTO mutation_writer
1517
+ FROM pg_proc procedure_row
1518
+ WHERE procedure_row.oid = to_regprocedure(format(
1519
+ '%I.scoped_knowledge_apply_lifecycle(uuid,text,uuid,text,bigint,text,text,text,text,text,text)',
1520
+ TG_TABLE_SCHEMA
1521
+ ));
1522
+ IF NOT coalesce(mutation_writer, false)
1523
+ OR current_setting('opengeni.knowledge_mutation_kind', true) <> 'lifecycle'
1524
+ OR current_setting('opengeni.knowledge_mutation_target', true) <> OLD.id::text
1525
+ OR (to_jsonb(NEW) - ARRAY['lifecycle_state', 'lifecycle_generation', 'updated_at'])
1526
+ IS DISTINCT FROM
1527
+ (to_jsonb(OLD) - ARRAY['lifecycle_state', 'lifecycle_generation', 'updated_at'])
1528
+ OR NEW.lifecycle_generation <> OLD.lifecycle_generation + 1
1529
+ OR NOT (
1530
+ (OLD.lifecycle_state = 'active' AND NEW.lifecycle_state IN ('deleted', 'revoked'))
1531
+ OR (OLD.lifecycle_state IN ('deleted', 'revoked') AND NEW.lifecycle_state = 'active')
1532
+ )
1533
+ THEN
1534
+ RAISE EXCEPTION 'invalid knowledge provider lifecycle mutation' USING ERRCODE = '23514';
1535
+ END IF;
1536
+ RETURN NEW;
1537
+ END;
1538
+ $$;
1539
+
1540
+ CREATE OR REPLACE FUNCTION scoped_knowledge_guard_source_mutation()
1541
+ RETURNS trigger LANGUAGE plpgsql AS $$
1542
+ DECLARE mutation_kind text := current_setting('opengeni.knowledge_mutation_kind', true);
1543
+ DECLARE mutation_writer boolean;
1544
+ BEGIN
1545
+ IF TG_OP = 'DELETE' THEN
1546
+ RAISE EXCEPTION 'knowledge source tombstones are not deleted' USING ERRCODE = '55000';
1547
+ END IF;
1548
+ SELECT current_user = pg_get_userbyid(procedure_row.proowner)
1549
+ INTO mutation_writer
1550
+ FROM pg_proc procedure_row
1551
+ WHERE procedure_row.oid = CASE mutation_kind
1552
+ WHEN 'lifecycle' THEN to_regprocedure(format(
1553
+ '%I.scoped_knowledge_apply_lifecycle(uuid,text,uuid,text,bigint,text,text,text,text,text,text)',
1554
+ TG_TABLE_SCHEMA
1555
+ ))
1556
+ WHEN 'acl' THEN to_regprocedure(format(
1557
+ '%I.scoped_knowledge_advance_source_acl(uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text)',
1558
+ TG_TABLE_SCHEMA
1559
+ ))
1560
+ WHEN 'sync' THEN to_regprocedure(format(
1561
+ '%I.scoped_knowledge_complete_sync(uuid,uuid,text,text,timestamptz,jsonb,text,text,text)',
1562
+ TG_TABLE_SCHEMA
1563
+ ))
1564
+ ELSE NULL
1565
+ END;
1566
+ IF NOT coalesce(mutation_writer, false)
1567
+ OR current_setting('opengeni.knowledge_mutation_target', true) <> OLD.id::text
1568
+ THEN
1569
+ RAISE EXCEPTION 'knowledge source mutation target is not fenced' USING ERRCODE = '55000';
1570
+ END IF;
1571
+ IF mutation_kind = 'lifecycle' THEN
1572
+ IF (to_jsonb(NEW) - ARRAY['lifecycle_state', 'lifecycle_generation', 'updated_at'])
1573
+ IS DISTINCT FROM
1574
+ (to_jsonb(OLD) - ARRAY['lifecycle_state', 'lifecycle_generation', 'updated_at'])
1575
+ OR NEW.lifecycle_generation <> OLD.lifecycle_generation + 1
1576
+ OR NOT (
1577
+ (OLD.lifecycle_state = 'active' AND NEW.lifecycle_state IN ('deleted', 'revoked'))
1578
+ OR (OLD.lifecycle_state IN ('deleted', 'revoked') AND NEW.lifecycle_state = 'active')
1579
+ )
1580
+ THEN
1581
+ RAISE EXCEPTION 'invalid knowledge source lifecycle mutation' USING ERRCODE = '23514';
1582
+ END IF;
1583
+ ELSIF mutation_kind = 'acl' THEN
1584
+ IF (to_jsonb(NEW) - ARRAY['current_acl_generation', 'updated_at'])
1585
+ IS DISTINCT FROM (to_jsonb(OLD) - ARRAY['current_acl_generation', 'updated_at'])
1586
+ OR NEW.lifecycle_state <> 'active'
1587
+ OR NEW.current_acl_generation IS NULL
1588
+ OR NEW.current_acl_generation <> coalesce(OLD.current_acl_generation, 0) + 1
1589
+ THEN
1590
+ RAISE EXCEPTION 'invalid knowledge source ACL mutation' USING ERRCODE = '23514';
1591
+ END IF;
1592
+ ELSIF mutation_kind = 'sync' THEN
1593
+ IF (to_jsonb(NEW) - ARRAY['sync_generation', 'sync_cursor', 'updated_at'])
1594
+ IS DISTINCT FROM (to_jsonb(OLD) - ARRAY['sync_generation', 'sync_cursor', 'updated_at'])
1595
+ OR NEW.lifecycle_state <> 'active'
1596
+ OR NEW.sync_generation <> OLD.sync_generation + 1
1597
+ THEN
1598
+ RAISE EXCEPTION 'invalid knowledge source sync mutation' USING ERRCODE = '23514';
1599
+ END IF;
1600
+ ELSE
1601
+ RAISE EXCEPTION 'knowledge source mutation kind is invalid' USING ERRCODE = '55000';
1602
+ END IF;
1603
+ RETURN NEW;
1604
+ END;
1605
+ $$;
1606
+
1607
+ CREATE OR REPLACE FUNCTION scoped_knowledge_guard_object_mutation()
1608
+ RETURNS trigger LANGUAGE plpgsql AS $$
1609
+ DECLARE mutation_kind text := current_setting('opengeni.knowledge_mutation_kind', true);
1610
+ DECLARE mutation_writer boolean;
1611
+ BEGIN
1612
+ IF TG_OP = 'DELETE' THEN
1613
+ RAISE EXCEPTION 'knowledge source object tombstones are not deleted' USING ERRCODE = '55000';
1614
+ END IF;
1615
+ SELECT current_user = pg_get_userbyid(procedure_row.proowner)
1616
+ INTO mutation_writer
1617
+ FROM pg_proc procedure_row
1618
+ WHERE procedure_row.oid = CASE mutation_kind
1619
+ WHEN 'lifecycle' THEN to_regprocedure(format(
1620
+ '%I.scoped_knowledge_apply_lifecycle(uuid,text,uuid,text,bigint,text,text,text,text,text,text)',
1621
+ TG_TABLE_SCHEMA
1622
+ ))
1623
+ WHEN 'version' THEN to_regprocedure(format(
1624
+ '%I.scoped_knowledge_advance_object_version(uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text)',
1625
+ TG_TABLE_SCHEMA
1626
+ ))
1627
+ ELSE NULL
1628
+ END;
1629
+ IF NOT coalesce(mutation_writer, false)
1630
+ OR current_setting('opengeni.knowledge_mutation_target', true) <> OLD.id::text
1631
+ THEN
1632
+ RAISE EXCEPTION 'knowledge source object mutation target is not fenced' USING ERRCODE = '55000';
1633
+ END IF;
1634
+ IF mutation_kind = 'lifecycle' THEN
1635
+ IF (to_jsonb(NEW) - ARRAY['lifecycle_state', 'lifecycle_generation', 'updated_at'])
1636
+ IS DISTINCT FROM
1637
+ (to_jsonb(OLD) - ARRAY['lifecycle_state', 'lifecycle_generation', 'updated_at'])
1638
+ OR NEW.lifecycle_generation <> OLD.lifecycle_generation + 1
1639
+ OR NOT (
1640
+ (OLD.lifecycle_state = 'active' AND NEW.lifecycle_state IN ('deleted', 'revoked'))
1641
+ OR (OLD.lifecycle_state IN ('deleted', 'revoked') AND NEW.lifecycle_state = 'active')
1642
+ )
1643
+ THEN
1644
+ RAISE EXCEPTION 'invalid knowledge source object lifecycle mutation'
1645
+ USING ERRCODE = '23514';
1646
+ END IF;
1647
+ ELSIF mutation_kind = 'version' THEN
1648
+ IF (to_jsonb(NEW) - ARRAY['version_generation', 'current_version_id', 'updated_at'])
1649
+ IS DISTINCT FROM (to_jsonb(OLD) - ARRAY['version_generation', 'current_version_id', 'updated_at'])
1650
+ OR NEW.lifecycle_state <> 'active'
1651
+ OR NEW.current_version_id IS NULL
1652
+ OR NEW.version_generation <> OLD.version_generation + 1
1653
+ THEN
1654
+ RAISE EXCEPTION 'invalid knowledge source object version mutation'
1655
+ USING ERRCODE = '23514';
1656
+ END IF;
1657
+ ELSE
1658
+ RAISE EXCEPTION 'knowledge source object mutation kind is invalid' USING ERRCODE = '55000';
1659
+ END IF;
1660
+ RETURN NEW;
1661
+ END;
1662
+ $$;
1663
+
1664
+ CREATE OR REPLACE FUNCTION scoped_knowledge_guard_sync_mutation()
1665
+ RETURNS trigger LANGUAGE plpgsql AS $$
1666
+ DECLARE mutation_writer boolean;
1667
+ BEGIN
1668
+ IF TG_OP = 'DELETE' THEN
1669
+ RAISE EXCEPTION 'knowledge sync runs are not deleted' USING ERRCODE = '55000';
1670
+ END IF;
1671
+ SELECT current_user = pg_get_userbyid(procedure_row.proowner)
1672
+ INTO mutation_writer
1673
+ FROM pg_proc procedure_row
1674
+ WHERE procedure_row.oid = to_regprocedure(format(
1675
+ '%I.scoped_knowledge_complete_sync(uuid,uuid,text,text,timestamptz,jsonb,text,text,text)',
1676
+ TG_TABLE_SCHEMA
1677
+ ));
1678
+ IF NOT coalesce(mutation_writer, false)
1679
+ OR current_setting('opengeni.knowledge_mutation_kind', true) <> 'sync_complete'
1680
+ OR current_setting('opengeni.knowledge_mutation_target', true) <> OLD.id::text
1681
+ OR (to_jsonb(NEW) - ARRAY[
1682
+ 'state', 'output_cursor', 'watermark', 'metadata', 'error_code',
1683
+ 'completion_hash', 'completed_at'
1684
+ ]) IS DISTINCT FROM (to_jsonb(OLD) - ARRAY[
1685
+ 'state', 'output_cursor', 'watermark', 'metadata', 'error_code',
1686
+ 'completion_hash', 'completed_at'
1687
+ ])
1688
+ OR OLD.state <> 'started'
1689
+ OR NEW.state NOT IN ('succeeded', 'failed')
1690
+ THEN
1691
+ RAISE EXCEPTION 'invalid knowledge sync completion mutation' USING ERRCODE = '23514';
1692
+ END IF;
1693
+ RETURN NEW;
1694
+ END;
1695
+ $$;
1696
+
1697
+ CREATE TRIGGER knowledge_providers_lifecycle_only
1698
+ BEFORE UPDATE OR DELETE ON "knowledge_providers"
1699
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_provider_mutation();
1700
+ CREATE TRIGGER knowledge_sources_mutation_guard
1701
+ BEFORE UPDATE OR DELETE ON "knowledge_sources"
1702
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_source_mutation();
1703
+ CREATE TRIGGER knowledge_source_objects_mutation_guard
1704
+ BEFORE UPDATE OR DELETE ON "knowledge_source_objects"
1705
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_object_mutation();
1706
+ CREATE TRIGGER knowledge_sync_runs_completion_only
1707
+ BEFORE UPDATE OR DELETE ON "knowledge_sync_runs"
1708
+ FOR EACH ROW EXECUTE FUNCTION scoped_knowledge_guard_sync_mutation();
1709
+
1710
+ DO $lifecycle_function$
1711
+ DECLARE data_schema text := current_schema();
1712
+ BEGIN
1713
+ EXECUTE format($ddl$
1714
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_apply_lifecycle(
1715
+ p_account_id uuid,
1716
+ p_target_kind text,
1717
+ p_target_id uuid,
1718
+ p_event_type text,
1719
+ p_expected_generation bigint,
1720
+ p_operation_id text,
1721
+ p_input_hash text,
1722
+ p_reason_code text,
1723
+ p_actor_kind text,
1724
+ p_actor_subject_id text,
1725
+ p_initiating_human_subject_id text
1726
+ ) RETURNS TABLE (
1727
+ target_id uuid,
1728
+ lifecycle_state text,
1729
+ lifecycle_generation bigint,
1730
+ replayed boolean
1731
+ )
1732
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
1733
+ AS $body$
1734
+ DECLARE target_row record;
1735
+ DECLARE prior_event knowledge_lifecycle_events%%ROWTYPE;
1736
+ DECLARE next_state text;
1737
+ DECLARE provider_ref uuid;
1738
+ DECLARE source_ref uuid;
1739
+ DECLARE object_ref uuid;
1740
+ BEGIN
1741
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
1742
+ OR p_target_kind NOT IN ('provider', 'source', 'object')
1743
+ OR p_event_type NOT IN ('deleted', 'revoked', 'restored')
1744
+ OR NOT opengeni_private.scoped_knowledge_actor_authorized(
1745
+ p_actor_kind, p_actor_subject_id, p_initiating_human_subject_id
1746
+ )
1747
+ THEN
1748
+ RAISE EXCEPTION 'scoped knowledge lifecycle authority is invalid'
1749
+ USING ERRCODE = '42501';
1750
+ END IF;
1751
+
1752
+ SELECT * INTO prior_event
1753
+ FROM knowledge_lifecycle_events event
1754
+ WHERE event.account_id = p_account_id
1755
+ AND event.target_kind = p_target_kind
1756
+ AND opengeni_private.scoped_knowledge_scope_visible(
1757
+ event.account_id, event.scope_kind, event.scope_workspace_id, event.scope_subject_id
1758
+ )
1759
+ AND event.provider_id IS NOT DISTINCT FROM
1760
+ CASE WHEN p_target_kind = 'provider' THEN p_target_id ELSE NULL END
1761
+ AND event.source_id IS NOT DISTINCT FROM
1762
+ CASE WHEN p_target_kind = 'source' THEN p_target_id ELSE NULL END
1763
+ AND event.object_id IS NOT DISTINCT FROM
1764
+ CASE WHEN p_target_kind = 'object' THEN p_target_id ELSE NULL END
1765
+ AND event.operation_id = p_operation_id;
1766
+ IF FOUND THEN
1767
+ IF prior_event.input_hash <> p_input_hash OR prior_event.event_type <> p_event_type THEN
1768
+ RAISE EXCEPTION 'knowledge lifecycle operation id was replayed with different input'
1769
+ USING ERRCODE = '23505';
1770
+ END IF;
1771
+ RETURN QUERY SELECT p_target_id, prior_event.new_state,
1772
+ prior_event.new_generation, true;
1773
+ RETURN;
1774
+ END IF;
1775
+
1776
+ IF p_target_kind = 'provider' THEN
1777
+ SELECT * INTO target_row FROM knowledge_providers row
1778
+ WHERE row.id = p_target_id AND row.account_id = p_account_id
1779
+ AND opengeni_private.scoped_knowledge_scope_visible(
1780
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
1781
+ )
1782
+ FOR UPDATE;
1783
+ provider_ref := p_target_id;
1784
+ ELSIF p_target_kind = 'source' THEN
1785
+ SELECT * INTO target_row FROM knowledge_sources row
1786
+ WHERE row.id = p_target_id AND row.account_id = p_account_id
1787
+ AND opengeni_private.scoped_knowledge_scope_visible(
1788
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
1789
+ )
1790
+ FOR UPDATE;
1791
+ source_ref := p_target_id;
1792
+ ELSE
1793
+ SELECT * INTO target_row FROM knowledge_source_objects row
1794
+ WHERE row.id = p_target_id AND row.account_id = p_account_id
1795
+ AND opengeni_private.scoped_knowledge_scope_visible(
1796
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
1797
+ )
1798
+ FOR UPDATE;
1799
+ object_ref := p_target_id;
1800
+ END IF;
1801
+ IF NOT FOUND THEN
1802
+ RAISE EXCEPTION 'knowledge lifecycle target was not found' USING ERRCODE = 'P0002';
1803
+ END IF;
1804
+ IF target_row.lifecycle_generation <> p_expected_generation THEN
1805
+ RAISE EXCEPTION 'knowledge lifecycle generation changed' USING ERRCODE = '40001';
1806
+ END IF;
1807
+ IF p_event_type = 'restored' THEN
1808
+ IF target_row.lifecycle_state NOT IN ('deleted', 'revoked') THEN
1809
+ RAISE EXCEPTION 'only a tombstoned knowledge target can be restored'
1810
+ USING ERRCODE = '23514';
1811
+ END IF;
1812
+ next_state := 'active';
1813
+ ELSE
1814
+ IF target_row.lifecycle_state <> 'active' THEN
1815
+ RAISE EXCEPTION 'ordinary lifecycle mutation cannot resurrect a knowledge tombstone'
1816
+ USING ERRCODE = '23514';
1817
+ END IF;
1818
+ next_state := CASE p_event_type WHEN 'deleted' THEN 'deleted' ELSE 'revoked' END;
1819
+ END IF;
1820
+
1821
+ PERFORM set_config('opengeni.knowledge_mutation_kind', 'lifecycle', true);
1822
+ PERFORM set_config('opengeni.knowledge_mutation_target', p_target_id::text, true);
1823
+ IF p_target_kind = 'provider' THEN
1824
+ UPDATE knowledge_providers target_provider SET lifecycle_state = next_state,
1825
+ lifecycle_generation = target_provider.lifecycle_generation + 1,
1826
+ updated_at = clock_timestamp()
1827
+ WHERE target_provider.id = p_target_id;
1828
+ ELSIF p_target_kind = 'source' THEN
1829
+ UPDATE knowledge_sources target_source SET lifecycle_state = next_state,
1830
+ lifecycle_generation = target_source.lifecycle_generation + 1,
1831
+ updated_at = clock_timestamp()
1832
+ WHERE target_source.id = p_target_id;
1833
+ ELSE
1834
+ UPDATE knowledge_source_objects target_object SET lifecycle_state = next_state,
1835
+ lifecycle_generation = target_object.lifecycle_generation + 1,
1836
+ updated_at = clock_timestamp()
1837
+ WHERE target_object.id = p_target_id;
1838
+ END IF;
1839
+ PERFORM set_config('opengeni.knowledge_mutation_kind', '', true);
1840
+ PERFORM set_config('opengeni.knowledge_mutation_target', '', true);
1841
+
1842
+ INSERT INTO knowledge_lifecycle_events (
1843
+ account_id, scope_kind, scope_workspace_id, scope_subject_id, scope_key,
1844
+ target_kind, provider_id, source_id, object_id, event_type,
1845
+ old_state, new_state, old_generation, new_generation,
1846
+ operation_id, input_hash, reason_code, actor_kind, actor_subject_id,
1847
+ initiating_human_subject_id
1848
+ ) VALUES (
1849
+ p_account_id, target_row.scope_kind, target_row.scope_workspace_id,
1850
+ target_row.scope_subject_id, target_row.scope_key,
1851
+ p_target_kind, provider_ref, source_ref, object_ref, p_event_type,
1852
+ target_row.lifecycle_state, next_state, target_row.lifecycle_generation,
1853
+ target_row.lifecycle_generation + 1, p_operation_id, p_input_hash,
1854
+ p_reason_code, p_actor_kind, p_actor_subject_id, p_initiating_human_subject_id
1855
+ );
1856
+ RETURN QUERY SELECT p_target_id, next_state,
1857
+ target_row.lifecycle_generation + 1, false;
1858
+ END;
1859
+ $body$;
1860
+ $ddl$, data_schema);
1861
+ END
1862
+ $lifecycle_function$;
1863
+
1864
+ DO $source_acl_function$
1865
+ DECLARE data_schema text := current_schema();
1866
+ BEGIN
1867
+ EXECUTE format($ddl$
1868
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_advance_source_acl(
1869
+ p_account_id uuid,
1870
+ p_source_id uuid,
1871
+ p_expected_lifecycle_generation bigint,
1872
+ p_expected_acl_generation bigint,
1873
+ p_acl_version_id uuid,
1874
+ p_operation_id text,
1875
+ p_input_hash text,
1876
+ p_reason_code text,
1877
+ p_actor_kind text,
1878
+ p_actor_subject_id text,
1879
+ p_initiating_human_subject_id text
1880
+ ) RETURNS bigint
1881
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
1882
+ AS $body$
1883
+ DECLARE source_row knowledge_sources%%ROWTYPE;
1884
+ DECLARE acl_row knowledge_source_acl_versions%%ROWTYPE;
1885
+ BEGIN
1886
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
1887
+ OR NOT opengeni_private.scoped_knowledge_actor_authorized(
1888
+ p_actor_kind, p_actor_subject_id, p_initiating_human_subject_id
1889
+ )
1890
+ THEN
1891
+ RAISE EXCEPTION 'knowledge ACL account authority is invalid' USING ERRCODE = '42501';
1892
+ END IF;
1893
+ SELECT * INTO source_row FROM knowledge_sources row
1894
+ WHERE row.id = p_source_id AND row.account_id = p_account_id
1895
+ AND opengeni_private.scoped_knowledge_scope_visible(
1896
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
1897
+ )
1898
+ FOR UPDATE;
1899
+ IF NOT FOUND THEN RAISE EXCEPTION 'knowledge source was not found' USING ERRCODE = 'P0002'; END IF;
1900
+ SELECT * INTO acl_row FROM knowledge_source_acl_versions row
1901
+ WHERE row.id = p_acl_version_id AND row.account_id = p_account_id
1902
+ AND row.source_id = p_source_id AND row.operation_id = p_operation_id
1903
+ AND opengeni_private.scoped_knowledge_scope_visible(
1904
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
1905
+ );
1906
+ IF NOT FOUND THEN RAISE EXCEPTION 'knowledge ACL version was not found' USING ERRCODE = 'P0002'; END IF;
1907
+ IF source_row.current_acl_generation = acl_row.generation THEN
1908
+ IF acl_row.input_hash <> p_input_hash THEN
1909
+ RAISE EXCEPTION 'knowledge ACL operation id was replayed with different input'
1910
+ USING ERRCODE = '23505';
1911
+ END IF;
1912
+ RETURN acl_row.generation;
1913
+ END IF;
1914
+ IF source_row.lifecycle_state <> 'active'
1915
+ OR source_row.lifecycle_generation <> p_expected_lifecycle_generation
1916
+ OR coalesce(source_row.current_acl_generation, 0) <> p_expected_acl_generation
1917
+ OR acl_row.generation <> p_expected_acl_generation + 1
1918
+ OR acl_row.input_hash <> p_input_hash
1919
+ OR acl_row.actor_kind IS DISTINCT FROM p_actor_kind
1920
+ OR acl_row.actor_subject_id IS DISTINCT FROM p_actor_subject_id
1921
+ OR acl_row.initiating_human_subject_id IS DISTINCT FROM p_initiating_human_subject_id
1922
+ THEN
1923
+ RAISE EXCEPTION 'knowledge ACL generation or lifecycle fence changed'
1924
+ USING ERRCODE = '40001';
1925
+ END IF;
1926
+ PERFORM set_config('opengeni.knowledge_mutation_kind', 'acl', true);
1927
+ PERFORM set_config('opengeni.knowledge_mutation_target', p_source_id::text, true);
1928
+ UPDATE knowledge_sources SET current_acl_generation = acl_row.generation,
1929
+ updated_at = clock_timestamp() WHERE id = p_source_id;
1930
+ PERFORM set_config('opengeni.knowledge_mutation_kind', '', true);
1931
+ PERFORM set_config('opengeni.knowledge_mutation_target', '', true);
1932
+ INSERT INTO knowledge_lifecycle_events (
1933
+ account_id, scope_kind, scope_workspace_id, scope_subject_id, scope_key,
1934
+ target_kind, source_id, event_type, old_state, new_state,
1935
+ old_generation, new_generation, operation_id, input_hash, reason_code,
1936
+ actor_kind, actor_subject_id, initiating_human_subject_id
1937
+ ) VALUES (
1938
+ p_account_id, source_row.scope_kind, source_row.scope_workspace_id,
1939
+ source_row.scope_subject_id, source_row.scope_key, 'source', p_source_id,
1940
+ 'acl_changed', source_row.lifecycle_state, source_row.lifecycle_state,
1941
+ source_row.lifecycle_generation, source_row.lifecycle_generation,
1942
+ p_operation_id, p_input_hash, p_reason_code, p_actor_kind,
1943
+ p_actor_subject_id, p_initiating_human_subject_id
1944
+ );
1945
+ RETURN acl_row.generation;
1946
+ END;
1947
+ $body$;
1948
+ $ddl$, data_schema);
1949
+ END
1950
+ $source_acl_function$;
1951
+
1952
+ DO $sync_function$
1953
+ DECLARE data_schema text := current_schema();
1954
+ BEGIN
1955
+ EXECUTE format($ddl$
1956
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_complete_sync(
1957
+ p_account_id uuid,
1958
+ p_run_id uuid,
1959
+ p_state text,
1960
+ p_output_cursor text,
1961
+ p_watermark timestamptz,
1962
+ p_metadata jsonb,
1963
+ p_error_code text,
1964
+ p_completion_hash text,
1965
+ p_reason_code text
1966
+ ) RETURNS SETOF %1$I.knowledge_sync_runs
1967
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
1968
+ AS $body$
1969
+ DECLARE run_row knowledge_sync_runs%%ROWTYPE;
1970
+ DECLARE source_row knowledge_sources%%ROWTYPE;
1971
+ BEGIN
1972
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
1973
+ OR p_state NOT IN ('succeeded', 'failed')
1974
+ THEN
1975
+ RAISE EXCEPTION 'knowledge sync completion authority is invalid' USING ERRCODE = '42501';
1976
+ END IF;
1977
+ SELECT * INTO run_row FROM knowledge_sync_runs row
1978
+ WHERE row.id = p_run_id AND row.account_id = p_account_id
1979
+ AND opengeni_private.scoped_knowledge_scope_visible(
1980
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
1981
+ )
1982
+ FOR UPDATE;
1983
+ IF NOT FOUND THEN RAISE EXCEPTION 'knowledge sync run was not found' USING ERRCODE = 'P0002'; END IF;
1984
+ IF NOT opengeni_private.scoped_knowledge_actor_authorized(
1985
+ run_row.actor_kind, run_row.actor_subject_id, run_row.initiating_human_subject_id
1986
+ ) THEN
1987
+ RAISE EXCEPTION 'knowledge sync completion actor authority changed'
1988
+ USING ERRCODE = '42501';
1989
+ END IF;
1990
+ IF run_row.state <> 'started' THEN
1991
+ IF run_row.state <> p_state OR run_row.completion_hash <> p_completion_hash THEN
1992
+ RAISE EXCEPTION 'knowledge sync completion was replayed with different input'
1993
+ USING ERRCODE = '23505';
1994
+ END IF;
1995
+ RETURN NEXT run_row;
1996
+ RETURN;
1997
+ END IF;
1998
+ SELECT * INTO source_row FROM knowledge_sources row
1999
+ WHERE row.id = run_row.source_id AND row.account_id = p_account_id
2000
+ AND opengeni_private.scoped_knowledge_scope_visible(
2001
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
2002
+ )
2003
+ FOR UPDATE;
2004
+ IF NOT FOUND OR source_row.lifecycle_state <> 'active'
2005
+ OR source_row.lifecycle_generation <> run_row.input_lifecycle_generation
2006
+ OR source_row.sync_generation <> run_row.input_sync_generation
2007
+ THEN
2008
+ RAISE EXCEPTION 'knowledge sync generation or lifecycle fence changed'
2009
+ USING ERRCODE = '40001';
2010
+ END IF;
2011
+ IF p_state = 'succeeded' THEN
2012
+ PERFORM set_config('opengeni.knowledge_mutation_kind', 'sync', true);
2013
+ PERFORM set_config('opengeni.knowledge_mutation_target', source_row.id::text, true);
2014
+ UPDATE knowledge_sources SET sync_generation = sync_generation + 1,
2015
+ sync_cursor = p_output_cursor, updated_at = clock_timestamp()
2016
+ WHERE id = source_row.id;
2017
+ END IF;
2018
+ PERFORM set_config('opengeni.knowledge_mutation_kind', 'sync_complete', true);
2019
+ PERFORM set_config('opengeni.knowledge_mutation_target', run_row.id::text, true);
2020
+ UPDATE knowledge_sync_runs SET state = p_state,
2021
+ output_cursor = CASE WHEN p_state = 'succeeded' THEN p_output_cursor ELSE NULL END,
2022
+ watermark = p_watermark, metadata = p_metadata,
2023
+ error_code = CASE WHEN p_state = 'failed' THEN p_error_code ELSE NULL END,
2024
+ completion_hash = p_completion_hash, completed_at = clock_timestamp()
2025
+ WHERE id = run_row.id RETURNING * INTO run_row;
2026
+ PERFORM set_config('opengeni.knowledge_mutation_kind', '', true);
2027
+ PERFORM set_config('opengeni.knowledge_mutation_target', '', true);
2028
+ INSERT INTO knowledge_lifecycle_events (
2029
+ account_id, scope_kind, scope_workspace_id, scope_subject_id, scope_key,
2030
+ target_kind, source_id, event_type, old_state, new_state,
2031
+ old_generation, new_generation, operation_id, input_hash, reason_code,
2032
+ actor_kind, actor_subject_id, initiating_human_subject_id
2033
+ ) VALUES (
2034
+ p_account_id, source_row.scope_kind, source_row.scope_workspace_id,
2035
+ source_row.scope_subject_id, source_row.scope_key, 'source', source_row.id,
2036
+ CASE WHEN p_state = 'succeeded' THEN 'sync_succeeded' ELSE 'sync_failed' END,
2037
+ source_row.lifecycle_state, source_row.lifecycle_state,
2038
+ source_row.lifecycle_generation, source_row.lifecycle_generation,
2039
+ run_row.operation_id, p_completion_hash, p_reason_code,
2040
+ run_row.actor_kind, run_row.actor_subject_id, run_row.initiating_human_subject_id
2041
+ );
2042
+ RETURN NEXT run_row;
2043
+ END;
2044
+ $body$;
2045
+ $ddl$, data_schema);
2046
+ END
2047
+ $sync_function$;
2048
+
2049
+ DO $object_version_function$
2050
+ DECLARE data_schema text := current_schema();
2051
+ BEGIN
2052
+ EXECUTE format($ddl$
2053
+ CREATE OR REPLACE FUNCTION %1$I.scoped_knowledge_advance_object_version(
2054
+ p_account_id uuid,
2055
+ p_object_id uuid,
2056
+ p_expected_lifecycle_generation bigint,
2057
+ p_expected_version_generation bigint,
2058
+ p_version_id uuid,
2059
+ p_operation_id text,
2060
+ p_input_hash text,
2061
+ p_reason_code text,
2062
+ p_actor_kind text,
2063
+ p_actor_subject_id text,
2064
+ p_initiating_human_subject_id text
2065
+ ) RETURNS bigint
2066
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %1$I, pg_catalog
2067
+ AS $body$
2068
+ DECLARE object_row knowledge_source_objects%%ROWTYPE;
2069
+ DECLARE version_row knowledge_document_versions%%ROWTYPE;
2070
+ DECLARE source_row knowledge_sources%%ROWTYPE;
2071
+ BEGIN
2072
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
2073
+ OR NOT opengeni_private.scoped_knowledge_actor_authorized(
2074
+ p_actor_kind, p_actor_subject_id, p_initiating_human_subject_id
2075
+ )
2076
+ THEN
2077
+ RAISE EXCEPTION 'knowledge object version account authority is invalid'
2078
+ USING ERRCODE = '42501';
2079
+ END IF;
2080
+ SELECT * INTO object_row FROM knowledge_source_objects row
2081
+ WHERE row.id = p_object_id AND row.account_id = p_account_id
2082
+ AND opengeni_private.scoped_knowledge_scope_visible(
2083
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
2084
+ )
2085
+ FOR UPDATE;
2086
+ IF NOT FOUND THEN RAISE EXCEPTION 'knowledge source object was not found' USING ERRCODE = 'P0002'; END IF;
2087
+ SELECT * INTO version_row FROM knowledge_document_versions row
2088
+ WHERE row.id = p_version_id AND row.account_id = p_account_id
2089
+ AND row.object_id = p_object_id AND row.operation_id = p_operation_id
2090
+ AND opengeni_private.scoped_knowledge_scope_visible(
2091
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
2092
+ );
2093
+ IF NOT FOUND THEN RAISE EXCEPTION 'knowledge document version was not found' USING ERRCODE = 'P0002'; END IF;
2094
+ IF object_row.current_version_id = version_row.id THEN
2095
+ IF version_row.input_hash <> p_input_hash THEN
2096
+ RAISE EXCEPTION 'knowledge document version operation was replayed with different input'
2097
+ USING ERRCODE = '23505';
2098
+ END IF;
2099
+ RETURN version_row.version_generation;
2100
+ END IF;
2101
+ SELECT * INTO source_row FROM knowledge_sources row
2102
+ WHERE row.id = version_row.source_id AND row.account_id = p_account_id
2103
+ AND opengeni_private.scoped_knowledge_scope_visible(
2104
+ row.account_id, row.scope_kind, row.scope_workspace_id, row.scope_subject_id
2105
+ )
2106
+ FOR UPDATE;
2107
+ IF NOT FOUND THEN
2108
+ RAISE EXCEPTION 'knowledge document version source was not found'
2109
+ USING ERRCODE = 'P0002';
2110
+ END IF;
2111
+ IF object_row.lifecycle_state <> 'active'
2112
+ OR object_row.lifecycle_generation <> p_expected_lifecycle_generation
2113
+ OR object_row.version_generation <> p_expected_version_generation
2114
+ OR source_row.lifecycle_state <> 'active'
2115
+ OR source_row.current_acl_generation IS DISTINCT FROM version_row.acl_generation
2116
+ OR version_row.version_generation <> p_expected_version_generation + 1
2117
+ OR version_row.input_hash <> p_input_hash
2118
+ OR version_row.actor_kind IS DISTINCT FROM p_actor_kind
2119
+ OR version_row.actor_subject_id IS DISTINCT FROM p_actor_subject_id
2120
+ OR version_row.initiating_human_subject_id IS DISTINCT FROM p_initiating_human_subject_id
2121
+ THEN
2122
+ RAISE EXCEPTION 'knowledge object version or lifecycle fence changed'
2123
+ USING ERRCODE = '40001';
2124
+ END IF;
2125
+ PERFORM set_config('opengeni.knowledge_mutation_kind', 'version', true);
2126
+ PERFORM set_config('opengeni.knowledge_mutation_target', p_object_id::text, true);
2127
+ UPDATE knowledge_source_objects SET current_version_id = version_row.id,
2128
+ version_generation = version_row.version_generation, updated_at = clock_timestamp()
2129
+ WHERE id = p_object_id;
2130
+ PERFORM set_config('opengeni.knowledge_mutation_kind', '', true);
2131
+ PERFORM set_config('opengeni.knowledge_mutation_target', '', true);
2132
+ INSERT INTO knowledge_lifecycle_events (
2133
+ account_id, scope_kind, scope_workspace_id, scope_subject_id, scope_key,
2134
+ target_kind, object_id, event_type, old_state, new_state,
2135
+ old_generation, new_generation, operation_id, input_hash, reason_code,
2136
+ actor_kind, actor_subject_id, initiating_human_subject_id
2137
+ ) VALUES (
2138
+ p_account_id, object_row.scope_kind, object_row.scope_workspace_id,
2139
+ object_row.scope_subject_id, object_row.scope_key, 'object', p_object_id,
2140
+ 'object_version_added', object_row.lifecycle_state, object_row.lifecycle_state,
2141
+ object_row.lifecycle_generation, object_row.lifecycle_generation,
2142
+ p_operation_id, p_input_hash, p_reason_code, p_actor_kind,
2143
+ p_actor_subject_id, p_initiating_human_subject_id
2144
+ );
2145
+ RETURN version_row.version_generation;
2146
+ END;
2147
+ $body$;
2148
+ $ddl$, data_schema);
2149
+ END
2150
+ $object_version_function$;
2151
+
2152
+ DO $revoke_public_function_access$
2153
+ DECLARE data_schema text := current_schema();
2154
+ BEGIN
2155
+ EXECUTE format(
2156
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_apply_lifecycle('
2157
+ || 'uuid,text,uuid,text,bigint,text,text,text,text,text,text) FROM PUBLIC',
2158
+ data_schema
2159
+ );
2160
+ EXECUTE format(
2161
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_advance_source_acl('
2162
+ || 'uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text) FROM PUBLIC',
2163
+ data_schema
2164
+ );
2165
+ EXECUTE format(
2166
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_complete_sync('
2167
+ || 'uuid,uuid,text,text,timestamptz,jsonb,text,text,text) FROM PUBLIC',
2168
+ data_schema
2169
+ );
2170
+ EXECUTE format(
2171
+ 'REVOKE ALL ON FUNCTION %I.scoped_knowledge_advance_object_version('
2172
+ || 'uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text) FROM PUBLIC',
2173
+ data_schema
2174
+ );
2175
+ END
2176
+ $revoke_public_function_access$;
2177
+
2178
+ DO $rls$
2179
+ DECLARE table_name text;
2180
+ BEGIN
2181
+ FOREACH table_name IN ARRAY ARRAY[
2182
+ 'knowledge_operation_receipts',
2183
+ 'knowledge_providers',
2184
+ 'knowledge_sources',
2185
+ 'knowledge_source_acl_versions',
2186
+ 'knowledge_sync_runs',
2187
+ 'knowledge_source_objects',
2188
+ 'knowledge_document_versions',
2189
+ 'knowledge_lifecycle_events',
2190
+ 'knowledge_entities',
2191
+ 'knowledge_entity_aliases',
2192
+ 'knowledge_facts',
2193
+ 'knowledge_claims',
2194
+ 'knowledge_claim_relations',
2195
+ 'knowledge_claim_evidence',
2196
+ 'knowledge_claim_reviews',
2197
+ 'knowledge_change_proposals'
2198
+ ] LOOP
2199
+ EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', table_name);
2200
+ EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', table_name);
2201
+ EXECUTE format(
2202
+ 'CREATE POLICY scoped_knowledge_select ON %I FOR SELECT '
2203
+ || 'USING (opengeni_private.scoped_knowledge_scope_visible('
2204
+ || 'account_id, scope_kind, scope_workspace_id, scope_subject_id))',
2205
+ table_name
2206
+ );
2207
+ EXECUTE format(
2208
+ 'CREATE POLICY scoped_knowledge_insert ON %I FOR INSERT '
2209
+ || 'WITH CHECK ('
2210
+ || 'opengeni_private.scoped_knowledge_scope_visible('
2211
+ || 'account_id, scope_kind, scope_workspace_id, scope_subject_id) '
2212
+ || 'AND opengeni_private.scoped_knowledge_actor_authorized('
2213
+ || 'actor_kind, actor_subject_id, initiating_human_subject_id))',
2214
+ table_name
2215
+ );
2216
+ END LOOP;
2217
+
2218
+ FOREACH table_name IN ARRAY ARRAY[
2219
+ 'knowledge_providers',
2220
+ 'knowledge_sources',
2221
+ 'knowledge_sync_runs',
2222
+ 'knowledge_source_objects'
2223
+ ] LOOP
2224
+ EXECUTE format(
2225
+ 'CREATE POLICY scoped_knowledge_update ON %I FOR UPDATE '
2226
+ || 'USING (opengeni_private.scoped_knowledge_scope_visible('
2227
+ || 'account_id, scope_kind, scope_workspace_id, scope_subject_id)) '
2228
+ || 'WITH CHECK (opengeni_private.scoped_knowledge_scope_visible('
2229
+ || 'account_id, scope_kind, scope_workspace_id, scope_subject_id))',
2230
+ table_name
2231
+ );
2232
+ END LOOP;
2233
+ END
2234
+ $rls$;
2235
+
2236
+ DO $runtime_grants$
2237
+ DECLARE table_name text;
2238
+ DECLARE data_schema text := current_schema();
2239
+ BEGIN
2240
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
2241
+ FOREACH table_name IN ARRAY ARRAY[
2242
+ 'knowledge_operation_receipts',
2243
+ 'knowledge_providers',
2244
+ 'knowledge_sources',
2245
+ 'knowledge_source_acl_versions',
2246
+ 'knowledge_sync_runs',
2247
+ 'knowledge_source_objects',
2248
+ 'knowledge_document_versions',
2249
+ 'knowledge_entities',
2250
+ 'knowledge_entity_aliases',
2251
+ 'knowledge_facts',
2252
+ 'knowledge_claims',
2253
+ 'knowledge_claim_relations',
2254
+ 'knowledge_claim_evidence',
2255
+ 'knowledge_claim_reviews',
2256
+ 'knowledge_change_proposals'
2257
+ ] LOOP
2258
+ EXECUTE format('GRANT SELECT, INSERT ON TABLE %I.%I TO opengeni_app', data_schema, table_name);
2259
+ END LOOP;
2260
+ EXECUTE format(
2261
+ 'GRANT SELECT ON TABLE %I.knowledge_lifecycle_events TO opengeni_app', data_schema
2262
+ );
2263
+ EXECUTE format(
2264
+ 'GRANT USAGE, SELECT ON SEQUENCE %I.knowledge_claim_review_revision_seq TO opengeni_app',
2265
+ data_schema
2266
+ );
2267
+ EXECUTE format(
2268
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_apply_lifecycle('
2269
+ || 'uuid,text,uuid,text,bigint,text,text,text,text,text,text) TO opengeni_app',
2270
+ data_schema
2271
+ );
2272
+ EXECUTE format(
2273
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_advance_source_acl('
2274
+ || 'uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text) TO opengeni_app',
2275
+ data_schema
2276
+ );
2277
+ EXECUTE format(
2278
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_complete_sync('
2279
+ || 'uuid,uuid,text,text,timestamptz,jsonb,text,text,text) TO opengeni_app',
2280
+ data_schema
2281
+ );
2282
+ EXECUTE format(
2283
+ 'GRANT EXECUTE ON FUNCTION %I.scoped_knowledge_advance_object_version('
2284
+ || 'uuid,uuid,bigint,bigint,uuid,text,text,text,text,text,text) TO opengeni_app',
2285
+ data_schema
2286
+ );
2287
+ END IF;
2288
+ END
2289
+ $runtime_grants$;
2290
+
2291
+ RESET statement_timeout;
2292
+ RESET lock_timeout;