@team-semicolon/semicolony-cli 4.18.51 → 4.18.52

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,600 @@
1
+ -- @schema-retarget: off
2
+ -- Independent, side-effect-free Hive cron shadow cursors and bounded observations.
3
+ -- The migration runner supplies the transaction boundary; do not add one here.
4
+
5
+ DO $$
6
+ DECLARE
7
+ role_state record;
8
+ BEGIN
9
+ SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole,
10
+ rolinherit, rolreplication, rolbypassrls
11
+ INTO role_state
12
+ FROM pg_roles
13
+ WHERE rolname = 'sc_provider';
14
+
15
+ IF NOT FOUND THEN
16
+ RAISE EXCEPTION 'sc_provider role missing; migration 135 required';
17
+ END IF;
18
+ IF NOT role_state.rolcanlogin
19
+ OR role_state.rolsuper
20
+ OR role_state.rolcreatedb
21
+ OR role_state.rolcreaterole
22
+ OR role_state.rolinherit
23
+ OR role_state.rolreplication
24
+ OR role_state.rolbypassrls THEN
25
+ RAISE EXCEPTION 'sc_provider role attributes are unsafe';
26
+ END IF;
27
+ END $$;
28
+
29
+ CREATE FUNCTION semicolony.hive_cron_shadow_sha256(p_value text)
30
+ RETURNS text
31
+ LANGUAGE sql
32
+ IMMUTABLE
33
+ STRICT
34
+ SECURITY DEFINER
35
+ SET search_path = pg_catalog
36
+ SET TimeZone = 'UTC'
37
+ AS $$
38
+ SELECT 'sha256:' || pg_catalog.encode(
39
+ pg_catalog.sha256(pg_catalog.convert_to(p_value, 'UTF8')),
40
+ 'hex'
41
+ );
42
+ $$;
43
+
44
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_shadow_sha256(text)
45
+ FROM PUBLIC, sc_app, sc_provider;
46
+
47
+ CREATE TABLE semicolony.hive_cron_shadow_cursors (
48
+ bot_id TEXT NOT NULL CHECK (bot_id = 'semi'),
49
+ job_id TEXT NOT NULL CHECK (
50
+ (job_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
51
+ ),
52
+ runtime_source TEXT NOT NULL CHECK (runtime_source = 'hive-cron-v2'),
53
+ cursor_version BIGINT NOT NULL CHECK (cursor_version > 0),
54
+ next_scheduled_for TIMESTAMPTZ NOT NULL,
55
+ schedule_digest TEXT NOT NULL CHECK (schedule_digest ~ '^sha256:[a-f0-9]{64}$'),
56
+ template_digest TEXT NOT NULL CHECK (template_digest ~ '^sha256:[a-f0-9]{64}$'),
57
+ binding_digest TEXT NOT NULL CHECK (binding_digest ~ '^sha256:[a-f0-9]{64}$'),
58
+ created_at TIMESTAMPTZ NOT NULL,
59
+ updated_at TIMESTAMPTZ NOT NULL,
60
+ PRIMARY KEY (bot_id, job_id, runtime_source),
61
+ FOREIGN KEY (bot_id, job_id, runtime_source)
62
+ REFERENCES semicolony.cron_capability_templates (bot_id, job_id, runtime_source)
63
+ );
64
+
65
+ COMMENT ON TABLE semicolony.hive_cron_shadow_cursors IS
66
+ 'Independent Hive shadow schedule cursor; never copies or advances primary next_run.';
67
+
68
+ CREATE TABLE semicolony.hive_cron_shadow_observations (
69
+ observation_id TEXT PRIMARY KEY CHECK (
70
+ observation_id = 'shadow-' || pg_catalog.substr(
71
+ semicolony.hive_cron_shadow_sha256(
72
+ bot_id || E'\n' || job_id || E'\n' || runtime_source || E'\n' || after_cursor_version::text
73
+ ),
74
+ 8
75
+ )
76
+ ),
77
+ bot_id TEXT NOT NULL CHECK (bot_id = 'semi'),
78
+ job_id TEXT NOT NULL CHECK (
79
+ (job_id COLLATE "C") ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
80
+ ),
81
+ runtime_source TEXT NOT NULL CHECK (runtime_source = 'hive-cron-v2'),
82
+ transition TEXT NOT NULL CHECK (transition IN ('initialized', 'advanced')),
83
+ before_cursor_version BIGINT,
84
+ before_next_scheduled_for TIMESTAMPTZ,
85
+ after_cursor_version BIGINT NOT NULL CHECK (after_cursor_version > 0),
86
+ after_next_scheduled_for TIMESTAMPTZ NOT NULL,
87
+ observed_at TIMESTAMPTZ NOT NULL,
88
+ due_count INTEGER NOT NULL CHECK (due_count BETWEEN 0 AND 512),
89
+ skipped_count INTEGER NOT NULL CHECK (skipped_count BETWEEN 0 AND 511),
90
+ enabled BOOLEAN NOT NULL,
91
+ schedule_digest TEXT NOT NULL CHECK (schedule_digest ~ '^sha256:[a-f0-9]{64}$'),
92
+ template_digest TEXT NOT NULL CHECK (template_digest ~ '^sha256:[a-f0-9]{64}$'),
93
+ binding_digest TEXT NOT NULL CHECK (binding_digest ~ '^sha256:[a-f0-9]{64}$'),
94
+ occurrence_digest TEXT NOT NULL CHECK (occurrence_digest ~ '^sha256:[a-f0-9]{64}$'),
95
+ observation_digest TEXT NOT NULL CHECK (observation_digest ~ '^sha256:[a-f0-9]{64}$'),
96
+ UNIQUE (bot_id, job_id, runtime_source, after_cursor_version),
97
+ FOREIGN KEY (bot_id, job_id, runtime_source)
98
+ REFERENCES semicolony.cron_capability_templates (bot_id, job_id, runtime_source),
99
+ CHECK (after_next_scheduled_for > observed_at),
100
+ CHECK (
101
+ (
102
+ transition = 'initialized'
103
+ AND before_cursor_version IS NULL
104
+ AND before_next_scheduled_for IS NULL
105
+ AND after_cursor_version = 1
106
+ AND due_count = 0
107
+ AND skipped_count = 0
108
+ )
109
+ OR
110
+ (
111
+ transition = 'advanced'
112
+ AND before_cursor_version IS NOT NULL
113
+ AND before_cursor_version > 0
114
+ AND before_next_scheduled_for IS NOT NULL
115
+ AND before_next_scheduled_for <= observed_at
116
+ AND after_cursor_version = before_cursor_version + 1
117
+ AND due_count BETWEEN 1 AND 512
118
+ AND skipped_count = due_count - 1
119
+ )
120
+ )
121
+ );
122
+
123
+ COMMENT ON TABLE semicolony.hive_cron_shadow_observations IS
124
+ 'Append-only bounded shadow cursor transitions without raw schedules, templates, or payloads.';
125
+
126
+ ALTER TABLE semicolony.hive_cron_shadow_cursors
127
+ ADD CONSTRAINT hive_cron_shadow_cursor_current_observation_fk
128
+ FOREIGN KEY (bot_id, job_id, runtime_source, cursor_version)
129
+ REFERENCES semicolony.hive_cron_shadow_observations
130
+ (bot_id, job_id, runtime_source, after_cursor_version)
131
+ DEFERRABLE INITIALLY DEFERRED;
132
+
133
+ CREATE FUNCTION semicolony.hive_cron_protect_shadow_observation()
134
+ RETURNS trigger
135
+ LANGUAGE plpgsql
136
+ SECURITY DEFINER
137
+ SET search_path = pg_catalog
138
+ SET TimeZone = 'UTC'
139
+ AS $$
140
+ BEGIN
141
+ RAISE EXCEPTION 'shadow_observation_immutable' USING ERRCODE = '55000';
142
+ END;
143
+ $$;
144
+
145
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_protect_shadow_observation()
146
+ FROM PUBLIC, sc_app, sc_provider;
147
+
148
+ CREATE TRIGGER protect_hive_cron_shadow_observation
149
+ BEFORE UPDATE OR DELETE ON semicolony.hive_cron_shadow_observations
150
+ FOR EACH ROW EXECUTE FUNCTION semicolony.hive_cron_protect_shadow_observation();
151
+
152
+ CREATE FUNCTION semicolony.hive_cron_advance_shadow(p_now timestamptz)
153
+ RETURNS TABLE (
154
+ transition text,
155
+ observation_id text,
156
+ bot_id text,
157
+ job_id text,
158
+ runtime_source text,
159
+ before_cursor_version bigint,
160
+ before_next_scheduled_for timestamptz,
161
+ after_cursor_version bigint,
162
+ after_next_scheduled_for timestamptz,
163
+ observed_at timestamptz,
164
+ due_count integer,
165
+ skipped_count integer,
166
+ enabled boolean,
167
+ schedule_digest text,
168
+ template_digest text,
169
+ binding_digest text,
170
+ occurrence_digest text,
171
+ observation_digest text
172
+ )
173
+ LANGUAGE plpgsql
174
+ SECURITY DEFINER
175
+ SET search_path = pg_catalog
176
+ SET TimeZone = 'UTC'
177
+ AS $$
178
+ DECLARE
179
+ candidate record;
180
+ candidates jsonb;
181
+ cursor_state record;
182
+ db_now timestamptz;
183
+ first_future timestamptz;
184
+ second_future timestamptz;
185
+ occurrence timestamptz;
186
+ previous_occurrence timestamptz;
187
+ occurrence_lines text[];
188
+ transition_value text;
189
+ observation_id_value text;
190
+ before_cursor_version_value bigint;
191
+ before_next_scheduled_for_value timestamptz;
192
+ after_cursor_version_value bigint;
193
+ after_next_scheduled_for_value timestamptz;
194
+ due_count_value integer;
195
+ skipped_count_value integer;
196
+ occurrence_digest_value text;
197
+ observation_digest_value text;
198
+ changed integer;
199
+ BEGIN
200
+ IF session_user <> 'sc_provider'
201
+ OR public.is_provider() IS DISTINCT FROM true THEN
202
+ RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
203
+ END IF;
204
+
205
+ PERFORM pg_catalog.pg_advisory_xact_lock(
206
+ pg_catalog.hashtextextended('semicolony.hive_cron_shadow_cursor', 0)
207
+ );
208
+ db_now := pg_catalog.clock_timestamp();
209
+ IF p_now IS NULL
210
+ OR p_now < db_now - interval '30 seconds'
211
+ OR p_now > db_now + interval '30 seconds' THEN
212
+ RAISE EXCEPTION 'invalid_shadow_clock' USING ERRCODE = '22023';
213
+ END IF;
214
+
215
+ SELECT COALESCE(
216
+ pg_catalog.jsonb_agg(
217
+ pg_catalog.jsonb_build_object(
218
+ 'bot_id', candidate_source.bot_id,
219
+ 'job_id', candidate_source.job_id,
220
+ 'runtime_source', candidate_source.runtime_source,
221
+ 'job_runtime_source', candidate_source.job_runtime_source,
222
+ 'schedule', candidate_source.schedule,
223
+ 'enabled', candidate_source.enabled,
224
+ 'schedule_digest', candidate_source.schedule_digest,
225
+ 'template_digest', candidate_source.template_digest,
226
+ 'binding_digest', semicolony.hive_cron_shadow_sha256(
227
+ candidate_source.schedule_digest || E'\n' ||
228
+ candidate_source.template_digest
229
+ )
230
+ ) ORDER BY candidate_source.job_id COLLATE "C"
231
+ ),
232
+ '[]'::jsonb
233
+ )
234
+ INTO candidates
235
+ FROM (
236
+ SELECT template.bot_id,
237
+ template.job_id,
238
+ template.runtime_source,
239
+ job.runtime_source AS job_runtime_source,
240
+ job.schedule,
241
+ job.enabled,
242
+ digests.schedule_digest,
243
+ digests.template_digest
244
+ FROM (
245
+ SELECT template_bound.*
246
+ FROM semicolony.cron_capability_templates AS template_bound
247
+ WHERE template_bound.bot_id = 'semi'
248
+ AND template_bound.runtime_source = 'hive-cron-v2'
249
+ ORDER BY template_bound.job_id COLLATE "C"
250
+ LIMIT 21
251
+ ) AS template
252
+ LEFT JOIN semicolony.bot_cron_jobs AS job
253
+ ON job.bot_id = template.bot_id
254
+ AND job.job_id = template.job_id
255
+ CROSS JOIN LATERAL (
256
+ SELECT semicolony.hive_cron_shadow_sha256(job.schedule::text) AS schedule_digest,
257
+ semicolony.hive_cron_shadow_sha256(
258
+ pg_catalog.to_jsonb(template)::text
259
+ ) AS template_digest
260
+ ) AS digests
261
+ ) AS candidate_source;
262
+
263
+ IF pg_catalog.jsonb_array_length(candidates) > 20 THEN
264
+ RAISE EXCEPTION 'shadow_candidate_limit_exceeded' USING ERRCODE = '54000';
265
+ END IF;
266
+
267
+ FOR candidate IN
268
+ SELECT candidate_row.*
269
+ FROM pg_catalog.jsonb_to_recordset(candidates) AS candidate_row(
270
+ bot_id text,
271
+ job_id text,
272
+ runtime_source text,
273
+ job_runtime_source text,
274
+ schedule jsonb,
275
+ enabled boolean,
276
+ schedule_digest text,
277
+ template_digest text,
278
+ binding_digest text
279
+ )
280
+ ORDER BY candidate_row.job_id COLLATE "C"
281
+ LOOP
282
+ IF candidate.job_runtime_source IS DISTINCT FROM candidate.runtime_source THEN
283
+ RAISE EXCEPTION 'shadow_binding_drift' USING ERRCODE = '40001';
284
+ END IF;
285
+ SELECT cursor.schedule_digest,
286
+ cursor.template_digest,
287
+ cursor.binding_digest
288
+ INTO cursor_state
289
+ FROM semicolony.hive_cron_shadow_cursors AS cursor
290
+ WHERE cursor.bot_id = candidate.bot_id
291
+ AND cursor.job_id = candidate.job_id
292
+ AND cursor.runtime_source = candidate.runtime_source;
293
+ IF FOUND AND (
294
+ cursor_state.schedule_digest IS DISTINCT FROM candidate.schedule_digest
295
+ OR cursor_state.template_digest IS DISTINCT FROM candidate.template_digest
296
+ OR cursor_state.binding_digest IS DISTINCT FROM candidate.binding_digest
297
+ ) THEN
298
+ RAISE EXCEPTION 'shadow_binding_drift' USING ERRCODE = '40001';
299
+ END IF;
300
+ BEGIN
301
+ first_future := semicolony.hive_cron_derive_next_run(candidate.schedule, db_now);
302
+ second_future := semicolony.hive_cron_derive_next_run(candidate.schedule, first_future);
303
+ EXCEPTION
304
+ WHEN SQLSTATE '22023' THEN
305
+ RAISE EXCEPTION 'shadow_schedule_frequency_invalid' USING ERRCODE = '22023';
306
+ END;
307
+ IF first_future <= db_now
308
+ OR second_future <= first_future
309
+ OR second_future - first_future < interval '30 minutes' THEN
310
+ RAISE EXCEPTION 'shadow_schedule_frequency_invalid' USING ERRCODE = '22023';
311
+ END IF;
312
+ END LOOP;
313
+
314
+ FOR candidate IN
315
+ SELECT candidate_row.*
316
+ FROM pg_catalog.jsonb_to_recordset(candidates) AS candidate_row(
317
+ bot_id text,
318
+ job_id text,
319
+ runtime_source text,
320
+ job_runtime_source text,
321
+ schedule jsonb,
322
+ enabled boolean,
323
+ schedule_digest text,
324
+ template_digest text,
325
+ binding_digest text
326
+ )
327
+ ORDER BY candidate_row.job_id COLLATE "C"
328
+ LOOP
329
+ SELECT cursor.cursor_version,
330
+ cursor.next_scheduled_for,
331
+ cursor.schedule_digest,
332
+ cursor.template_digest,
333
+ cursor.binding_digest
334
+ INTO cursor_state
335
+ FROM semicolony.hive_cron_shadow_cursors AS cursor
336
+ WHERE cursor.bot_id = candidate.bot_id
337
+ AND cursor.job_id = candidate.job_id
338
+ AND cursor.runtime_source = candidate.runtime_source
339
+ FOR UPDATE;
340
+
341
+ occurrence_lines := ARRAY[]::text[];
342
+ due_count_value := 0;
343
+ skipped_count_value := 0;
344
+ before_cursor_version_value := NULL;
345
+ before_next_scheduled_for_value := NULL;
346
+
347
+ IF NOT FOUND THEN
348
+ transition_value := 'initialized';
349
+ after_cursor_version_value := 1;
350
+ after_next_scheduled_for_value := semicolony.hive_cron_derive_next_run(
351
+ candidate.schedule,
352
+ db_now
353
+ );
354
+ occurrence_digest_value := semicolony.hive_cron_shadow_sha256('');
355
+
356
+ INSERT INTO semicolony.hive_cron_shadow_cursors (
357
+ bot_id, job_id, runtime_source, cursor_version, next_scheduled_for,
358
+ schedule_digest, template_digest, binding_digest, created_at, updated_at
359
+ ) VALUES (
360
+ candidate.bot_id, candidate.job_id, candidate.runtime_source,
361
+ after_cursor_version_value, after_next_scheduled_for_value,
362
+ candidate.schedule_digest, candidate.template_digest, candidate.binding_digest,
363
+ db_now, db_now
364
+ );
365
+ ELSE
366
+ IF cursor_state.schedule_digest IS DISTINCT FROM candidate.schedule_digest
367
+ OR cursor_state.template_digest IS DISTINCT FROM candidate.template_digest
368
+ OR cursor_state.binding_digest IS DISTINCT FROM candidate.binding_digest THEN
369
+ RAISE EXCEPTION 'shadow_binding_drift' USING ERRCODE = '40001';
370
+ END IF;
371
+ IF cursor_state.next_scheduled_for > db_now THEN
372
+ CONTINUE;
373
+ END IF;
374
+ IF db_now - cursor_state.next_scheduled_for > interval '8 days' THEN
375
+ RAISE EXCEPTION 'shadow_backlog_window_exceeded' USING ERRCODE = '54000';
376
+ END IF;
377
+
378
+ transition_value := 'advanced';
379
+ before_cursor_version_value := cursor_state.cursor_version;
380
+ before_next_scheduled_for_value := cursor_state.next_scheduled_for;
381
+ occurrence := cursor_state.next_scheduled_for;
382
+ WHILE occurrence <= db_now LOOP
383
+ due_count_value := due_count_value + 1;
384
+ IF due_count_value > 512 THEN
385
+ RAISE EXCEPTION 'shadow_occurrence_limit_exceeded' USING ERRCODE = '54000';
386
+ END IF;
387
+ occurrence_lines := pg_catalog.array_append(
388
+ occurrence_lines,
389
+ pg_catalog.to_char(
390
+ occurrence AT TIME ZONE 'UTC',
391
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
392
+ )
393
+ );
394
+ previous_occurrence := occurrence;
395
+ occurrence := semicolony.hive_cron_derive_next_run(candidate.schedule, occurrence);
396
+ IF occurrence <= previous_occurrence THEN
397
+ RAISE EXCEPTION 'shadow_non_monotonic_schedule' USING ERRCODE = '22023';
398
+ END IF;
399
+ END LOOP;
400
+
401
+ skipped_count_value := due_count_value - 1;
402
+ after_cursor_version_value := cursor_state.cursor_version + 1;
403
+ after_next_scheduled_for_value := occurrence;
404
+ occurrence_digest_value := semicolony.hive_cron_shadow_sha256(
405
+ pg_catalog.array_to_string(occurrence_lines, E'\n')
406
+ );
407
+
408
+ UPDATE semicolony.hive_cron_shadow_cursors AS cursor
409
+ SET cursor_version = after_cursor_version_value,
410
+ next_scheduled_for = after_next_scheduled_for_value,
411
+ updated_at = db_now
412
+ WHERE cursor.bot_id = candidate.bot_id
413
+ AND cursor.job_id = candidate.job_id
414
+ AND cursor.runtime_source = candidate.runtime_source
415
+ AND cursor.cursor_version = before_cursor_version_value
416
+ AND cursor.next_scheduled_for = before_next_scheduled_for_value
417
+ AND cursor.schedule_digest = candidate.schedule_digest
418
+ AND cursor.template_digest = candidate.template_digest
419
+ AND cursor.binding_digest = candidate.binding_digest;
420
+ GET DIAGNOSTICS changed = ROW_COUNT;
421
+ IF changed <> 1 THEN
422
+ RAISE EXCEPTION 'shadow_cursor_concurrent_change' USING ERRCODE = '40001';
423
+ END IF;
424
+ END IF;
425
+
426
+ observation_id_value := 'shadow-' || pg_catalog.substr(
427
+ semicolony.hive_cron_shadow_sha256(
428
+ candidate.bot_id || E'\n' || candidate.job_id || E'\n' ||
429
+ candidate.runtime_source || E'\n' || after_cursor_version_value::text
430
+ ),
431
+ 8
432
+ );
433
+ observation_digest_value := semicolony.hive_cron_shadow_sha256(
434
+ transition_value || E'\n' ||
435
+ COALESCE(before_cursor_version_value::text, '') || E'\n' ||
436
+ COALESCE(pg_catalog.to_char(
437
+ before_next_scheduled_for_value AT TIME ZONE 'UTC',
438
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
439
+ ), '') || E'\n' ||
440
+ after_cursor_version_value::text || E'\n' ||
441
+ pg_catalog.to_char(
442
+ after_next_scheduled_for_value AT TIME ZONE 'UTC',
443
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"'
444
+ ) || E'\n' ||
445
+ pg_catalog.to_char(db_now AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') || E'\n' ||
446
+ due_count_value::text || E'\n' || skipped_count_value::text || E'\n' ||
447
+ candidate.enabled::text || E'\n' || candidate.schedule_digest || E'\n' ||
448
+ candidate.template_digest || E'\n' || candidate.binding_digest || E'\n' ||
449
+ occurrence_digest_value
450
+ );
451
+
452
+ INSERT INTO semicolony.hive_cron_shadow_observations (
453
+ observation_id, bot_id, job_id, runtime_source, transition,
454
+ before_cursor_version, before_next_scheduled_for,
455
+ after_cursor_version, after_next_scheduled_for, observed_at,
456
+ due_count, skipped_count, enabled, schedule_digest, template_digest,
457
+ binding_digest, occurrence_digest, observation_digest
458
+ ) VALUES (
459
+ observation_id_value, candidate.bot_id, candidate.job_id, candidate.runtime_source,
460
+ transition_value, before_cursor_version_value, before_next_scheduled_for_value,
461
+ after_cursor_version_value, after_next_scheduled_for_value, db_now,
462
+ due_count_value, skipped_count_value, candidate.enabled,
463
+ candidate.schedule_digest, candidate.template_digest, candidate.binding_digest,
464
+ occurrence_digest_value, observation_digest_value
465
+ );
466
+
467
+ transition := transition_value;
468
+ observation_id := observation_id_value;
469
+ bot_id := candidate.bot_id;
470
+ job_id := candidate.job_id;
471
+ runtime_source := candidate.runtime_source;
472
+ before_cursor_version := before_cursor_version_value;
473
+ before_next_scheduled_for := before_next_scheduled_for_value;
474
+ after_cursor_version := after_cursor_version_value;
475
+ after_next_scheduled_for := after_next_scheduled_for_value;
476
+ observed_at := db_now;
477
+ due_count := due_count_value;
478
+ skipped_count := skipped_count_value;
479
+ enabled := candidate.enabled;
480
+ schedule_digest := candidate.schedule_digest;
481
+ template_digest := candidate.template_digest;
482
+ binding_digest := candidate.binding_digest;
483
+ occurrence_digest := occurrence_digest_value;
484
+ observation_digest := observation_digest_value;
485
+ RETURN NEXT;
486
+ END LOOP;
487
+ END;
488
+ $$;
489
+
490
+ CREATE FUNCTION semicolony.hive_cron_get_shadow_snapshot()
491
+ RETURNS TABLE (
492
+ bot_id text,
493
+ job_id text,
494
+ runtime_source text,
495
+ cursor_version bigint,
496
+ next_scheduled_for timestamptz,
497
+ cursor_schedule_digest text,
498
+ cursor_template_digest text,
499
+ cursor_binding_digest text,
500
+ created_at timestamptz,
501
+ updated_at timestamptz,
502
+ observation_id text,
503
+ transition text,
504
+ observation_before_cursor_version bigint,
505
+ observation_before_next_scheduled_for timestamptz,
506
+ observation_after_cursor_version bigint,
507
+ observation_after_next_scheduled_for timestamptz,
508
+ observed_at timestamptz,
509
+ due_count integer,
510
+ skipped_count integer,
511
+ enabled boolean,
512
+ observation_schedule_digest text,
513
+ observation_template_digest text,
514
+ observation_binding_digest text,
515
+ occurrence_digest text,
516
+ observation_digest text
517
+ )
518
+ LANGUAGE plpgsql
519
+ SECURITY DEFINER
520
+ SET search_path = pg_catalog
521
+ SET TimeZone = 'UTC'
522
+ AS $$
523
+ BEGIN
524
+ IF session_user <> 'sc_provider'
525
+ OR public.is_provider() IS DISTINCT FROM true THEN
526
+ RAISE EXCEPTION 'provider_mode_required' USING ERRCODE = '42501';
527
+ END IF;
528
+
529
+ PERFORM pg_catalog.pg_advisory_xact_lock(
530
+ pg_catalog.hashtextextended('semicolony.hive_cron_shadow_cursor', 0)
531
+ );
532
+ IF EXISTS (
533
+ SELECT 1
534
+ FROM semicolony.hive_cron_shadow_cursors AS cursor
535
+ LEFT JOIN semicolony.hive_cron_shadow_observations AS observation
536
+ ON observation.bot_id = cursor.bot_id
537
+ AND observation.job_id = cursor.job_id
538
+ AND observation.runtime_source = cursor.runtime_source
539
+ AND observation.after_cursor_version = cursor.cursor_version
540
+ WHERE observation.observation_id IS NULL
541
+ OR cursor.cursor_version IS DISTINCT FROM observation.after_cursor_version
542
+ OR cursor.next_scheduled_for IS DISTINCT FROM observation.after_next_scheduled_for
543
+ OR cursor.updated_at IS DISTINCT FROM observation.observed_at
544
+ OR cursor.schedule_digest IS DISTINCT FROM observation.schedule_digest
545
+ OR cursor.template_digest IS DISTINCT FROM observation.template_digest
546
+ OR cursor.binding_digest IS DISTINCT FROM observation.binding_digest
547
+ ) THEN
548
+ RAISE EXCEPTION 'shadow_snapshot_incoherent' USING ERRCODE = '40001';
549
+ END IF;
550
+
551
+ RETURN QUERY
552
+ SELECT cursor.bot_id,
553
+ cursor.job_id,
554
+ cursor.runtime_source,
555
+ cursor.cursor_version,
556
+ cursor.next_scheduled_for,
557
+ cursor.schedule_digest,
558
+ cursor.template_digest,
559
+ cursor.binding_digest,
560
+ cursor.created_at,
561
+ cursor.updated_at,
562
+ observation.observation_id,
563
+ observation.transition,
564
+ observation.before_cursor_version,
565
+ observation.before_next_scheduled_for,
566
+ observation.after_cursor_version,
567
+ observation.after_next_scheduled_for,
568
+ observation.observed_at,
569
+ observation.due_count,
570
+ observation.skipped_count,
571
+ observation.enabled,
572
+ observation.schedule_digest,
573
+ observation.template_digest,
574
+ observation.binding_digest,
575
+ observation.occurrence_digest,
576
+ observation.observation_digest
577
+ FROM semicolony.hive_cron_shadow_cursors AS cursor
578
+ JOIN semicolony.hive_cron_shadow_observations AS observation
579
+ ON observation.bot_id = cursor.bot_id
580
+ AND observation.job_id = cursor.job_id
581
+ AND observation.runtime_source = cursor.runtime_source
582
+ AND observation.after_cursor_version = cursor.cursor_version
583
+ ORDER BY cursor.job_id COLLATE "C";
584
+ END;
585
+ $$;
586
+
587
+ REVOKE ALL PRIVILEGES ON TABLE semicolony.hive_cron_shadow_cursors
588
+ FROM PUBLIC, sc_app, sc_provider;
589
+ REVOKE ALL PRIVILEGES ON TABLE semicolony.hive_cron_shadow_observations
590
+ FROM PUBLIC, sc_app, sc_provider;
591
+
592
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_advance_shadow(timestamptz)
593
+ FROM PUBLIC, sc_app, sc_provider;
594
+ REVOKE ALL ON FUNCTION semicolony.hive_cron_get_shadow_snapshot()
595
+ FROM PUBLIC, sc_app, sc_provider;
596
+
597
+ GRANT EXECUTE ON FUNCTION semicolony.hive_cron_advance_shadow(timestamptz)
598
+ TO sc_provider;
599
+ GRANT EXECUTE ON FUNCTION semicolony.hive_cron_get_shadow_snapshot()
600
+ TO sc_provider;