@defrex/autobuild-postgres-store 0.8.0 → 0.9.0
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.
- package/README.md +48 -0
- package/package.json +18 -1
- package/src/auth-schema.ts +21 -6
- package/src/retry.ts +260 -0
- package/src/schema.ts +437 -31
- package/src/store.ts +526 -197
- package/src/tickets.ts +88 -59
- package/src/blobs.test.ts +0 -121
- package/src/config.test.ts +0 -174
- package/src/env.test.ts +0 -48
- package/src/event-wait-poll-ms.test.ts +0 -23
- package/src/migrate.test.ts +0 -640
- package/src/store.live.test.ts +0 -453
- package/src/testing/concurrent-worker.ts +0 -42
- package/src/tickets.live.test.ts +0 -49
package/src/schema.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
assertAuthSchema,
|
|
9
9
|
} from './auth-schema'
|
|
10
10
|
|
|
11
|
-
export const SCHEMA_VERSION =
|
|
11
|
+
export const SCHEMA_VERSION = 9
|
|
12
12
|
export const MIGRATE_COMMAND = 'bun run postgres:migrate (from a pinned Autobuild release checkout)'
|
|
13
13
|
|
|
14
14
|
/** The frozen v3 DDL, kept verbatim so a deployed v3 marker's checksum can be
|
|
@@ -376,6 +376,296 @@ CREATE TABLE IF NOT EXISTS stream_chunks (
|
|
|
376
376
|
);`.trim()
|
|
377
377
|
export const SCHEMA_V5_CHECKSUM = new Bun.CryptoHasher('sha256').update(SCHEMA_V5_DDL).digest('hex')
|
|
378
378
|
|
|
379
|
+
/** The frozen v6 DDL, kept verbatim so a deployed v6 marker's checksum can be
|
|
380
|
+
* recognized and upgraded in place (see migratePostgres). v6 is the shape the
|
|
381
|
+
* hosted service ran immediately before the build-digest scan index (v7). */
|
|
382
|
+
export const SCHEMA_V6_DDL = `
|
|
383
|
+
CREATE TABLE IF NOT EXISTS ab_schema_migrations (
|
|
384
|
+
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
|
385
|
+
version integer NOT NULL,
|
|
386
|
+
checksum text NOT NULL,
|
|
387
|
+
applied_at timestamptz NOT NULL
|
|
388
|
+
);
|
|
389
|
+
CREATE TABLE IF NOT EXISTS builds (
|
|
390
|
+
slug text PRIMARY KEY, repo text NOT NULL, ticket jsonb, branch text,
|
|
391
|
+
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
|
|
392
|
+
lease_holder text, lease_expires_at timestamptz, lease_ttl_ms bigint,
|
|
393
|
+
heartbeat_at timestamptz, repo_origin text
|
|
394
|
+
);
|
|
395
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
396
|
+
build text NOT NULL REFERENCES builds(slug) ON DELETE CASCADE,
|
|
397
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
398
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (build, seq)
|
|
399
|
+
);
|
|
400
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
401
|
+
build text NOT NULL REFERENCES builds(slug) ON DELETE CASCADE,
|
|
402
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
403
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
404
|
+
PRIMARY KEY (build, kind, revision)
|
|
405
|
+
);
|
|
406
|
+
CREATE TABLE IF NOT EXISTS repo_streams (
|
|
407
|
+
repo text PRIMARY KEY, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
|
|
408
|
+
lease_holder text, lease_expires_at timestamptz, lease_ttl_ms bigint,
|
|
409
|
+
heartbeat_at timestamptz
|
|
410
|
+
);
|
|
411
|
+
CREATE TABLE IF NOT EXISTS repo_events (
|
|
412
|
+
repo text NOT NULL REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
413
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
414
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (repo, seq)
|
|
415
|
+
);
|
|
416
|
+
CREATE TABLE IF NOT EXISTS repo_artifacts (
|
|
417
|
+
repo text NOT NULL REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
418
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
419
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
420
|
+
PRIMARY KEY (repo, kind, revision)
|
|
421
|
+
);
|
|
422
|
+
CREATE SEQUENCE IF NOT EXISTS sessions_creation_seq;
|
|
423
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
424
|
+
id text PRIMARY KEY,
|
|
425
|
+
repo text NOT NULL,
|
|
426
|
+
operator text NOT NULL,
|
|
427
|
+
title text,
|
|
428
|
+
created_at timestamptz NOT NULL,
|
|
429
|
+
updated_at timestamptz NOT NULL,
|
|
430
|
+
creation_seq bigint NOT NULL
|
|
431
|
+
);
|
|
432
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
433
|
+
session text NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
434
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
435
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (session, seq)
|
|
436
|
+
);
|
|
437
|
+
CREATE TABLE IF NOT EXISTS session_artifacts (
|
|
438
|
+
session text NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
439
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
440
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
441
|
+
PRIMARY KEY (session, kind, revision)
|
|
442
|
+
);
|
|
443
|
+
CREATE SEQUENCE IF NOT EXISTS streams_creation_seq;
|
|
444
|
+
CREATE TABLE IF NOT EXISTS streams (
|
|
445
|
+
id text PRIMARY KEY, scope_kind text NOT NULL,
|
|
446
|
+
build text REFERENCES builds(slug) ON DELETE CASCADE,
|
|
447
|
+
repo text REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
448
|
+
label text NOT NULL, format text NOT NULL, status text NOT NULL,
|
|
449
|
+
outcome text, artifact_kind text, artifact_revision bigint,
|
|
450
|
+
artifact_blob_ref text, created_at timestamptz NOT NULL, closed_at timestamptz,
|
|
451
|
+
session text REFERENCES sessions(id) ON DELETE CASCADE,
|
|
452
|
+
creation_seq bigint NOT NULL,
|
|
453
|
+
CONSTRAINT streams_scope_kind_check CHECK (scope_kind IN ('build','repo','session')),
|
|
454
|
+
CONSTRAINT streams_status_check CHECK (status IN ('open','closed')),
|
|
455
|
+
CONSTRAINT streams_scope_exactly_one_check CHECK (
|
|
456
|
+
(scope_kind = 'build' AND build IS NOT NULL AND repo IS NULL AND session IS NULL)
|
|
457
|
+
OR (scope_kind = 'repo' AND build IS NULL AND repo IS NOT NULL AND session IS NULL)
|
|
458
|
+
OR (scope_kind = 'session' AND build IS NULL AND repo IS NULL AND session IS NOT NULL)
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
CREATE TABLE IF NOT EXISTS stream_chunks (
|
|
462
|
+
stream text NOT NULL REFERENCES streams(id) ON DELETE CASCADE,
|
|
463
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, parts jsonb NOT NULL,
|
|
464
|
+
PRIMARY KEY (stream, seq)
|
|
465
|
+
);`.trim()
|
|
466
|
+
export const SCHEMA_V6_CHECKSUM = new Bun.CryptoHasher('sha256').update(SCHEMA_V6_DDL).digest('hex')
|
|
467
|
+
|
|
468
|
+
/** The frozen v7 DDL, kept verbatim so a deployed v7 marker's checksum can be
|
|
469
|
+
* recognized and upgraded in place (see migratePostgres). v7 is the shape the
|
|
470
|
+
* hosted service ran immediately before the repository-journal state-read
|
|
471
|
+
* index (v8, AUT-489). Frozen pre-trimmed (the V1–V6 pattern): a deployed
|
|
472
|
+
* marker's checksum is taken over the trimmed DDL, so an untrimmed constant
|
|
473
|
+
* would hash surrounding whitespace no deployed database ever carried and the
|
|
474
|
+
* promotion branch would never match. */
|
|
475
|
+
export const SCHEMA_V7_DDL = `
|
|
476
|
+
CREATE TABLE IF NOT EXISTS ab_schema_migrations (
|
|
477
|
+
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
|
478
|
+
version integer NOT NULL,
|
|
479
|
+
checksum text NOT NULL,
|
|
480
|
+
applied_at timestamptz NOT NULL
|
|
481
|
+
);
|
|
482
|
+
CREATE TABLE IF NOT EXISTS builds (
|
|
483
|
+
slug text PRIMARY KEY, repo text NOT NULL, ticket jsonb, branch text,
|
|
484
|
+
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
|
|
485
|
+
lease_holder text, lease_expires_at timestamptz, lease_ttl_ms bigint,
|
|
486
|
+
heartbeat_at timestamptz, repo_origin text
|
|
487
|
+
);
|
|
488
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
489
|
+
build text NOT NULL REFERENCES builds(slug) ON DELETE CASCADE,
|
|
490
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
491
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (build, seq)
|
|
492
|
+
);
|
|
493
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
494
|
+
build text NOT NULL REFERENCES builds(slug) ON DELETE CASCADE,
|
|
495
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
496
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
497
|
+
PRIMARY KEY (build, kind, revision)
|
|
498
|
+
);
|
|
499
|
+
CREATE TABLE IF NOT EXISTS repo_streams (
|
|
500
|
+
repo text PRIMARY KEY, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
|
|
501
|
+
lease_holder text, lease_expires_at timestamptz, lease_ttl_ms bigint,
|
|
502
|
+
heartbeat_at timestamptz
|
|
503
|
+
);
|
|
504
|
+
CREATE TABLE IF NOT EXISTS repo_events (
|
|
505
|
+
repo text NOT NULL REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
506
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
507
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (repo, seq)
|
|
508
|
+
);
|
|
509
|
+
CREATE TABLE IF NOT EXISTS repo_artifacts (
|
|
510
|
+
repo text NOT NULL REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
511
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
512
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
513
|
+
PRIMARY KEY (repo, kind, revision)
|
|
514
|
+
);
|
|
515
|
+
CREATE SEQUENCE IF NOT EXISTS sessions_creation_seq;
|
|
516
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
517
|
+
id text PRIMARY KEY,
|
|
518
|
+
repo text NOT NULL,
|
|
519
|
+
operator text NOT NULL,
|
|
520
|
+
title text,
|
|
521
|
+
created_at timestamptz NOT NULL,
|
|
522
|
+
updated_at timestamptz NOT NULL,
|
|
523
|
+
creation_seq bigint NOT NULL
|
|
524
|
+
);
|
|
525
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
526
|
+
session text NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
527
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
528
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (session, seq)
|
|
529
|
+
);
|
|
530
|
+
CREATE TABLE IF NOT EXISTS session_artifacts (
|
|
531
|
+
session text NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
532
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
533
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
534
|
+
PRIMARY KEY (session, kind, revision)
|
|
535
|
+
);
|
|
536
|
+
CREATE SEQUENCE IF NOT EXISTS streams_creation_seq;
|
|
537
|
+
CREATE TABLE IF NOT EXISTS streams (
|
|
538
|
+
id text PRIMARY KEY, scope_kind text NOT NULL,
|
|
539
|
+
build text REFERENCES builds(slug) ON DELETE CASCADE,
|
|
540
|
+
repo text REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
541
|
+
label text NOT NULL, format text NOT NULL, status text NOT NULL,
|
|
542
|
+
outcome text, artifact_kind text, artifact_revision bigint,
|
|
543
|
+
artifact_blob_ref text, created_at timestamptz NOT NULL, closed_at timestamptz,
|
|
544
|
+
session text REFERENCES sessions(id) ON DELETE CASCADE,
|
|
545
|
+
creation_seq bigint NOT NULL,
|
|
546
|
+
CONSTRAINT streams_scope_kind_check CHECK (scope_kind IN ('build','repo','session')),
|
|
547
|
+
CONSTRAINT streams_status_check CHECK (status IN ('open','closed')),
|
|
548
|
+
CONSTRAINT streams_scope_exactly_one_check CHECK (
|
|
549
|
+
(scope_kind = 'build' AND build IS NOT NULL AND repo IS NULL AND session IS NULL)
|
|
550
|
+
OR (scope_kind = 'repo' AND build IS NULL AND repo IS NOT NULL AND session IS NULL)
|
|
551
|
+
OR (scope_kind = 'session' AND build IS NULL AND repo IS NULL AND session IS NOT NULL)
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
CREATE TABLE IF NOT EXISTS stream_chunks (
|
|
555
|
+
stream text NOT NULL REFERENCES streams(id) ON DELETE CASCADE,
|
|
556
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, parts jsonb NOT NULL,
|
|
557
|
+
PRIMARY KEY (stream, seq)
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
-- The build-digest scan index (AUT-487): type-leading, so the digest query's
|
|
561
|
+
-- cost grows with the observation/terminal events themselves, not with total
|
|
562
|
+
-- history. Created idempotently so pre-v7 databases gain it on migrate.
|
|
563
|
+
CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq);`.trim()
|
|
564
|
+
export const SCHEMA_V7_CHECKSUM = new Bun.CryptoHasher('sha256').update(SCHEMA_V7_DDL).digest('hex')
|
|
565
|
+
|
|
566
|
+
/** The frozen v8 DDL, kept verbatim so a deployed v8 marker's checksum can be
|
|
567
|
+
* recognized and upgraded in place (see migratePostgres). v8 is the shape the
|
|
568
|
+
* hosted service ran immediately before ticket assets (v9). Frozen pre-trimmed
|
|
569
|
+
* (the V1–V7 pattern): a deployed marker's checksum is taken over the trimmed
|
|
570
|
+
* DDL. */
|
|
571
|
+
export const SCHEMA_V8_DDL = `
|
|
572
|
+
CREATE TABLE IF NOT EXISTS ab_schema_migrations (
|
|
573
|
+
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
|
574
|
+
version integer NOT NULL,
|
|
575
|
+
checksum text NOT NULL,
|
|
576
|
+
applied_at timestamptz NOT NULL
|
|
577
|
+
);
|
|
578
|
+
CREATE TABLE IF NOT EXISTS builds (
|
|
579
|
+
slug text PRIMARY KEY, repo text NOT NULL, ticket jsonb, branch text,
|
|
580
|
+
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
|
|
581
|
+
lease_holder text, lease_expires_at timestamptz, lease_ttl_ms bigint,
|
|
582
|
+
heartbeat_at timestamptz, repo_origin text
|
|
583
|
+
);
|
|
584
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
585
|
+
build text NOT NULL REFERENCES builds(slug) ON DELETE CASCADE,
|
|
586
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
587
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (build, seq)
|
|
588
|
+
);
|
|
589
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
590
|
+
build text NOT NULL REFERENCES builds(slug) ON DELETE CASCADE,
|
|
591
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
592
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
593
|
+
PRIMARY KEY (build, kind, revision)
|
|
594
|
+
);
|
|
595
|
+
CREATE TABLE IF NOT EXISTS repo_streams (
|
|
596
|
+
repo text PRIMARY KEY, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL,
|
|
597
|
+
lease_holder text, lease_expires_at timestamptz, lease_ttl_ms bigint,
|
|
598
|
+
heartbeat_at timestamptz
|
|
599
|
+
);
|
|
600
|
+
CREATE TABLE IF NOT EXISTS repo_events (
|
|
601
|
+
repo text NOT NULL REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
602
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
603
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (repo, seq)
|
|
604
|
+
);
|
|
605
|
+
CREATE TABLE IF NOT EXISTS repo_artifacts (
|
|
606
|
+
repo text NOT NULL REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
607
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
608
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
609
|
+
PRIMARY KEY (repo, kind, revision)
|
|
610
|
+
);
|
|
611
|
+
CREATE SEQUENCE IF NOT EXISTS sessions_creation_seq;
|
|
612
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
613
|
+
id text PRIMARY KEY,
|
|
614
|
+
repo text NOT NULL,
|
|
615
|
+
operator text NOT NULL,
|
|
616
|
+
title text,
|
|
617
|
+
created_at timestamptz NOT NULL,
|
|
618
|
+
updated_at timestamptz NOT NULL,
|
|
619
|
+
creation_seq bigint NOT NULL
|
|
620
|
+
);
|
|
621
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
622
|
+
session text NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
623
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, actor jsonb NOT NULL,
|
|
624
|
+
type text NOT NULL, payload jsonb NOT NULL, PRIMARY KEY (session, seq)
|
|
625
|
+
);
|
|
626
|
+
CREATE TABLE IF NOT EXISTS session_artifacts (
|
|
627
|
+
session text NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
628
|
+
kind text NOT NULL, revision bigint NOT NULL, blob_ref text NOT NULL,
|
|
629
|
+
metadata jsonb NOT NULL, created_at timestamptz NOT NULL,
|
|
630
|
+
PRIMARY KEY (session, kind, revision)
|
|
631
|
+
);
|
|
632
|
+
CREATE SEQUENCE IF NOT EXISTS streams_creation_seq;
|
|
633
|
+
CREATE TABLE IF NOT EXISTS streams (
|
|
634
|
+
id text PRIMARY KEY, scope_kind text NOT NULL,
|
|
635
|
+
build text REFERENCES builds(slug) ON DELETE CASCADE,
|
|
636
|
+
repo text REFERENCES repo_streams(repo) ON DELETE CASCADE,
|
|
637
|
+
label text NOT NULL, format text NOT NULL, status text NOT NULL,
|
|
638
|
+
outcome text, artifact_kind text, artifact_revision bigint,
|
|
639
|
+
artifact_blob_ref text, created_at timestamptz NOT NULL, closed_at timestamptz,
|
|
640
|
+
session text REFERENCES sessions(id) ON DELETE CASCADE,
|
|
641
|
+
creation_seq bigint NOT NULL,
|
|
642
|
+
CONSTRAINT streams_scope_kind_check CHECK (scope_kind IN ('build','repo','session')),
|
|
643
|
+
CONSTRAINT streams_status_check CHECK (status IN ('open','closed')),
|
|
644
|
+
CONSTRAINT streams_scope_exactly_one_check CHECK (
|
|
645
|
+
(scope_kind = 'build' AND build IS NOT NULL AND repo IS NULL AND session IS NULL)
|
|
646
|
+
OR (scope_kind = 'repo' AND build IS NULL AND repo IS NOT NULL AND session IS NULL)
|
|
647
|
+
OR (scope_kind = 'session' AND build IS NULL AND repo IS NULL AND session IS NOT NULL)
|
|
648
|
+
)
|
|
649
|
+
);
|
|
650
|
+
CREATE TABLE IF NOT EXISTS stream_chunks (
|
|
651
|
+
stream text NOT NULL REFERENCES streams(id) ON DELETE CASCADE,
|
|
652
|
+
seq bigint NOT NULL, ts timestamptz NOT NULL, parts jsonb NOT NULL,
|
|
653
|
+
PRIMARY KEY (stream, seq)
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
-- The build-digest scan index (AUT-487): type-leading, so the digest query's
|
|
657
|
+
-- cost grows with the observation/terminal events themselves, not with total
|
|
658
|
+
-- history. Created idempotently so pre-v7 databases gain it on migrate.
|
|
659
|
+
CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq);
|
|
660
|
+
|
|
661
|
+
-- The repository-journal state read index (AUT-489): (repo, type, seq), so
|
|
662
|
+
-- the bounded state read's anchor query — MAX(seq) over the repo's
|
|
663
|
+
-- run-started facts — and its durable-type select both scan only their own
|
|
664
|
+
-- signal, never the repo's whole journal. Created idempotently so pre-v8
|
|
665
|
+
-- databases gain it on migrate.
|
|
666
|
+
CREATE INDEX IF NOT EXISTS repo_events_type_repo_seq ON repo_events (repo, type, seq);`.trim()
|
|
667
|
+
export const SCHEMA_V8_CHECKSUM = new Bun.CryptoHasher('sha256').update(SCHEMA_V8_DDL).digest('hex')
|
|
668
|
+
|
|
379
669
|
export const SCHEMA_DDL = `
|
|
380
670
|
CREATE TABLE IF NOT EXISTS ab_schema_migrations (
|
|
381
671
|
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
|
@@ -459,6 +749,28 @@ CREATE TABLE IF NOT EXISTS stream_chunks (
|
|
|
459
749
|
stream text NOT NULL REFERENCES streams(id) ON DELETE CASCADE,
|
|
460
750
|
seq bigint NOT NULL, ts timestamptz NOT NULL, parts jsonb NOT NULL,
|
|
461
751
|
PRIMARY KEY (stream, seq)
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
-- The build-digest scan index (AUT-487): type-leading, so the digest query's
|
|
755
|
+
-- cost grows with the observation/terminal events themselves, not with total
|
|
756
|
+
-- history. Created idempotently so pre-v7 databases gain it on migrate.
|
|
757
|
+
CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq);
|
|
758
|
+
|
|
759
|
+
-- The repository-journal state read index (AUT-489): (repo, type, seq), so
|
|
760
|
+
-- the bounded state read's anchor query — MAX(seq) over the repo's
|
|
761
|
+
-- run-started facts — and its durable-type select both scan only their own
|
|
762
|
+
-- signal, never the repo's whole journal. Created idempotently so pre-v8
|
|
763
|
+
-- databases gain it on migrate.
|
|
764
|
+
CREATE INDEX IF NOT EXISTS repo_events_type_repo_seq ON repo_events (repo, type, seq);
|
|
765
|
+
-- Ticket assets (SPEC §7.1): one row per revision, manifest as jsonb, file
|
|
766
|
+
-- bytes in the blob store. A removal is a tombstone row (removed = true, empty
|
|
767
|
+
-- manifest) taking the next revision. No FK to repo_streams: assets can
|
|
768
|
+
-- precede any repo-journal row.
|
|
769
|
+
CREATE TABLE IF NOT EXISTS ticket_assets (
|
|
770
|
+
repo text NOT NULL, ticket_id text NOT NULL, kind text NOT NULL, name text NOT NULL,
|
|
771
|
+
revision bigint NOT NULL, layout text NOT NULL, manifest jsonb NOT NULL,
|
|
772
|
+
size bigint NOT NULL, removed boolean NOT NULL, created_at timestamptz NOT NULL,
|
|
773
|
+
PRIMARY KEY (repo, ticket_id, kind, name, revision)
|
|
462
774
|
);`.trim()
|
|
463
775
|
|
|
464
776
|
export const SCHEMA_CHECKSUM = new Bun.CryptoHasher('sha256').update(SCHEMA_DDL).digest('hex')
|
|
@@ -536,6 +848,18 @@ const EXPECTED_COLUMNS: Record<string, readonly ExpectedColumn[]> = {
|
|
|
536
848
|
['metadata', 'jsonb', true],
|
|
537
849
|
['created_at', 'timestamp with time zone', true],
|
|
538
850
|
],
|
|
851
|
+
ticket_assets: [
|
|
852
|
+
['repo', 'text', true],
|
|
853
|
+
['ticket_id', 'text', true],
|
|
854
|
+
['kind', 'text', true],
|
|
855
|
+
['name', 'text', true],
|
|
856
|
+
['revision', 'bigint', true],
|
|
857
|
+
['layout', 'text', true],
|
|
858
|
+
['manifest', 'jsonb', true],
|
|
859
|
+
['size', 'bigint', true],
|
|
860
|
+
['removed', 'boolean', true],
|
|
861
|
+
['created_at', 'timestamp with time zone', true],
|
|
862
|
+
],
|
|
539
863
|
repo_streams: [
|
|
540
864
|
['repo', 'text', true],
|
|
541
865
|
['created_at', 'timestamp with time zone', true],
|
|
@@ -647,12 +971,30 @@ const EXPECTED_CONSTRAINTS = [
|
|
|
647
971
|
'streams|f|repo|repo_streams|repo|c|',
|
|
648
972
|
'streams|f|session|sessions|id|c|',
|
|
649
973
|
'streams|p|id||||',
|
|
974
|
+
'ticket_assets|p|repo,ticket_id,kind,name,revision||||',
|
|
650
975
|
] as const
|
|
651
976
|
|
|
652
977
|
export function schemaError(detail: string): Error {
|
|
653
978
|
return new Error(`PostgreSQL BuildStore schema ${detail}; run: ${MIGRATE_COMMAND}`)
|
|
654
979
|
}
|
|
655
980
|
|
|
981
|
+
/** The targeted cause text for a marker whose version matches the required
|
|
982
|
+
* version but whose checksum does not: exactly the shape a checksum-only
|
|
983
|
+
* re-pin leaves deployed databases in (AUT-548) — and also the shape of a
|
|
984
|
+
* database built by a different build of the same version, which a marker
|
|
985
|
+
* alone cannot distinguish, so both causes are named. The guard's failure
|
|
986
|
+
* output teaches the four-step rule pre-ship; this diagnostic catches the
|
|
987
|
+
* checksum-only shortcut after the fact. */
|
|
988
|
+
function checksumMismatchCause(version: number): string {
|
|
989
|
+
return (
|
|
990
|
+
`its version matches ${version}, but its checksum does not match this build's DDL — the ` +
|
|
991
|
+
`current DDL was likely edited in place without bumping the version, or the database was ` +
|
|
992
|
+
`built by a different build of this same version. Never edit deployed DDL; follow the ` +
|
|
993
|
+
`four-step rule (freeze the previous DDL, add an upgrade branch in migratePostgres, bump ` +
|
|
994
|
+
`the version, re-pin in schema-guard.test.ts) and redeploy`
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
|
|
656
998
|
interface CatalogColumn {
|
|
657
999
|
table_name: string
|
|
658
1000
|
column_name: string
|
|
@@ -775,7 +1117,9 @@ export async function assertSchema(sql: SQL): Promise<void> {
|
|
|
775
1117
|
if (Number(marker.version) !== SCHEMA_VERSION) {
|
|
776
1118
|
throw schemaError(`version ${marker.version} does not match required version ${SCHEMA_VERSION}`)
|
|
777
1119
|
}
|
|
778
|
-
if (marker.checksum !== SCHEMA_CHECKSUM)
|
|
1120
|
+
if (marker.checksum !== SCHEMA_CHECKSUM) {
|
|
1121
|
+
throw schemaError(`checksum is mismatched: ${checksumMismatchCause(SCHEMA_VERSION)}`)
|
|
1122
|
+
}
|
|
779
1123
|
await assertCatalogShape(sql)
|
|
780
1124
|
}
|
|
781
1125
|
|
|
@@ -792,11 +1136,16 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
|
|
|
792
1136
|
}
|
|
793
1137
|
const marker = rows[0]
|
|
794
1138
|
if (!marker) throw schemaError('ticket marker is missing')
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1139
|
+
if (Number(marker.version) !== TICKET_SCHEMA_VERSION) {
|
|
1140
|
+
throw schemaError(
|
|
1141
|
+
`ticket marker is incompatible: version ${marker.version} does not match required version ` +
|
|
1142
|
+
`${TICKET_SCHEMA_VERSION}`,
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1145
|
+
if (marker.checksum !== TICKET_SCHEMA_CHECKSUM) {
|
|
1146
|
+
throw schemaError(
|
|
1147
|
+
`ticket marker is incompatible: ${checksumMismatchCause(TICKET_SCHEMA_VERSION)}`,
|
|
1148
|
+
)
|
|
800
1149
|
}
|
|
801
1150
|
const expected: Record<string, Array<[string, string, boolean]>> = {
|
|
802
1151
|
ab_ticket_schema_migrations: [
|
|
@@ -851,7 +1200,7 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
|
|
|
851
1200
|
}
|
|
852
1201
|
}
|
|
853
1202
|
|
|
854
|
-
/** The guarded streams.creation_seq upgrade, shared by every pre-
|
|
1203
|
+
/** The guarded streams.creation_seq upgrade, shared by every pre-v7 marker
|
|
855
1204
|
* branch whose database carries a pre-existing streams table (v2, v3, v4, v5).
|
|
856
1205
|
* The idempotent full DDL above has already created the
|
|
857
1206
|
* streams_creation_seq sequence; the column is added only when missing, with
|
|
@@ -891,12 +1240,12 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
891
1240
|
await tx`SELECT version, checksum FROM ab_schema_migrations WHERE singleton = true FOR UPDATE`
|
|
892
1241
|
const marker = rows[0]
|
|
893
1242
|
if (marker) {
|
|
894
|
-
// v1–
|
|
895
|
-
// pre-
|
|
1243
|
+
// v1–v7 → v9: the guarded builds.repo_origin column runs for EVERY
|
|
1244
|
+
// pre-v7 marker, before the version branches — not only in a v4
|
|
896
1245
|
// branch. `CREATE TABLE IF NOT EXISTS builds` does not alter an
|
|
897
1246
|
// existing builds table, so without this the internal assertSchema
|
|
898
1247
|
// below would fail on every upgraded legacy database. The ALTER is
|
|
899
|
-
// guarded and idempotent, so re-running it on a
|
|
1248
|
+
// guarded and idempotent, so re-running it on a v7 database (or a
|
|
900
1249
|
// marker that turns out to be rejected below, which rolls this
|
|
901
1250
|
// transaction back) is a no-op.
|
|
902
1251
|
await tx.unsafe(`
|
|
@@ -910,25 +1259,49 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
910
1259
|
END IF;
|
|
911
1260
|
END $$;
|
|
912
1261
|
`)
|
|
1262
|
+
// v6 → v7: the guarded build-digest scan index (AUT-487) likewise
|
|
1263
|
+
// runs for EVERY pre-v7 marker (and, with the state-read index below,
|
|
1264
|
+
// every pre-v8 marker) — the idempotent full DDL above only
|
|
1265
|
+
// creates an index on a database that lacks the events table, never
|
|
1266
|
+
// on a pre-existing one. Guarded and idempotent (the repo_origin
|
|
1267
|
+
// precedent), so it is a no-op on fresh installs, on v7 reruns, and
|
|
1268
|
+
// on markers rejected below (which roll the transaction back).
|
|
1269
|
+
await tx.unsafe(
|
|
1270
|
+
`CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq)`,
|
|
1271
|
+
)
|
|
1272
|
+
// v7 → v9: the guarded repository-journal state-read index (AUT-489)
|
|
1273
|
+
// likewise runs for EVERY pre-v8 marker — same reasoning as the
|
|
1274
|
+
// digest-scan index above. Guarded and idempotent, so it is a no-op
|
|
1275
|
+
// on fresh installs, on v8 reruns, and on markers rejected below
|
|
1276
|
+
// (which roll the transaction back). Indexes are not catalog-asserted,
|
|
1277
|
+
// so no EXPECTED_* change.
|
|
1278
|
+
await tx.unsafe(
|
|
1279
|
+
`CREATE INDEX IF NOT EXISTS repo_events_type_repo_seq ON repo_events (repo, type, seq)`,
|
|
1280
|
+
)
|
|
913
1281
|
const version = Number(marker.version)
|
|
914
1282
|
if (version === SCHEMA_VERSION) {
|
|
915
|
-
if (marker.checksum !== SCHEMA_CHECKSUM)
|
|
1283
|
+
if (marker.checksum !== SCHEMA_CHECKSUM) {
|
|
1284
|
+
throw schemaError(`marker is incompatible: ${checksumMismatchCause(SCHEMA_VERSION)}`)
|
|
1285
|
+
}
|
|
916
1286
|
} else if (version === 1 && marker.checksum === SCHEMA_V1_CHECKSUM) {
|
|
917
|
-
// v1 →
|
|
1287
|
+
// v1 → v9: the idempotent full DDL above already applied the deltas
|
|
918
1288
|
// (the stream tables and the session tables); v1 databases never had
|
|
919
1289
|
// a streams or sessions table, so the full DDL created them with the
|
|
920
1290
|
// session column, the widened CHECKs, and both creation_seq columns.
|
|
921
|
-
// The guarded repo_origin ALTER
|
|
922
|
-
// marker in this
|
|
1291
|
+
// The guarded repo_origin ALTER and the guarded digest-scan index
|
|
1292
|
+
// above covered the pre-existing tables. Promote the marker in this
|
|
1293
|
+
// transaction.
|
|
923
1294
|
await tx`UPDATE ab_schema_migrations
|
|
924
1295
|
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
925
1296
|
applied_at = ${new Date().toISOString()}
|
|
926
1297
|
WHERE singleton = true`
|
|
927
1298
|
} else if (version === 2 && marker.checksum === SCHEMA_V2_CHECKSUM) {
|
|
928
|
-
// v2 →
|
|
1299
|
+
// v2 → v9: the idempotent full DDL above created the session tables
|
|
929
1300
|
// (with the creation_seq column); a v2 database's streams table
|
|
930
1301
|
// needs the guarded session column, the widened CHECK constraints,
|
|
931
|
-
// and the guarded creation_seq column (shared with v3 and v4).
|
|
1302
|
+
// and the guarded creation_seq column (shared with v3 and v4). The
|
|
1303
|
+
// guarded repo_origin ALTER and the guarded digest-scan index above
|
|
1304
|
+
// covered the pre-existing builds and events tables.
|
|
932
1305
|
await tx.unsafe(`
|
|
933
1306
|
DO $$ BEGIN
|
|
934
1307
|
IF NOT EXISTS (
|
|
@@ -958,7 +1331,7 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
958
1331
|
applied_at = ${new Date().toISOString()}
|
|
959
1332
|
WHERE singleton = true`
|
|
960
1333
|
} else if (version === 3 && marker.checksum === SCHEMA_V3_CHECKSUM) {
|
|
961
|
-
// v3 →
|
|
1334
|
+
// v3 → v9: the listSessions and listStreams creation-order
|
|
962
1335
|
// tiebreaks (store/types.ts). The idempotent full DDL above created
|
|
963
1336
|
// the sessions_creation_seq and streams_creation_seq sequences; a
|
|
964
1337
|
// v3 database's sessions and streams tables need the guarded
|
|
@@ -967,8 +1340,8 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
967
1340
|
// unorderable, so any total order consistent with createdAt is
|
|
968
1341
|
// acceptable — SET NOT NULL, and the sequence positioned above the
|
|
969
1342
|
// backfilled values so the next nextval continues the counter
|
|
970
|
-
// without collision. The guarded repo_origin ALTER
|
|
971
|
-
//
|
|
1343
|
+
// without collision. The guarded repo_origin ALTER and the guarded
|
|
1344
|
+
// digest-scan index above covered the pre-existing tables.
|
|
972
1345
|
await tx.unsafe(`
|
|
973
1346
|
DO $$ BEGIN
|
|
974
1347
|
IF NOT EXISTS (
|
|
@@ -994,32 +1367,58 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
994
1367
|
applied_at = ${new Date().toISOString()}
|
|
995
1368
|
WHERE singleton = true`
|
|
996
1369
|
} else if (version === 4 && marker.checksum === SCHEMA_V4_CHECKSUM) {
|
|
997
|
-
// v4 →
|
|
1370
|
+
// v4 → v9: the listStreams creation-order tiebreak
|
|
998
1371
|
// (store/types.ts), mirroring the v3→v4 sessions treatment. The
|
|
999
1372
|
// idempotent full DDL above created the streams_creation_seq
|
|
1000
1373
|
// sequence; a v4 database's streams table needs the guarded
|
|
1001
1374
|
// creation_seq column, backfill, NOT NULL, and sequence continuity
|
|
1002
1375
|
// (the shared pre-v6 streams migration). The guarded repo_origin
|
|
1003
|
-
// ALTER above covered
|
|
1376
|
+
// ALTER and the guarded digest-scan index above covered the
|
|
1377
|
+
// pre-existing tables.
|
|
1004
1378
|
await tx.unsafe(STREAMS_CREATION_SEQ_MIGRATION)
|
|
1005
1379
|
await tx`UPDATE ab_schema_migrations
|
|
1006
1380
|
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1007
1381
|
applied_at = ${new Date().toISOString()}
|
|
1008
1382
|
WHERE singleton = true`
|
|
1009
1383
|
} else if (version === 5 && marker.checksum === SCHEMA_V5_CHECKSUM) {
|
|
1010
|
-
// v5 →
|
|
1384
|
+
// v5 → v9: the listStreams creation-order tiebreak (store/types.ts)
|
|
1011
1385
|
// landed on the v5 DDL without a version bump, so the hosted
|
|
1012
1386
|
// database carries the v5 marker with the pre-tiebreak checksum. The
|
|
1013
1387
|
// idempotent full DDL above created the streams_creation_seq
|
|
1014
1388
|
// sequence; a v5 database's streams table needs the guarded
|
|
1015
1389
|
// creation_seq column, backfill, NOT NULL, and sequence continuity
|
|
1016
|
-
// (the shared pre-v6 streams migration). Every other
|
|
1017
|
-
// column already exists in a v5 database
|
|
1390
|
+
// (the shared pre-v6 streams migration). Every other pre-v7 table
|
|
1391
|
+
// and column already exists in a v5 database; the guarded
|
|
1392
|
+
// digest-scan index above covers events.
|
|
1018
1393
|
await tx.unsafe(STREAMS_CREATION_SEQ_MIGRATION)
|
|
1019
1394
|
await tx`UPDATE ab_schema_migrations
|
|
1020
1395
|
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1021
1396
|
applied_at = ${new Date().toISOString()}
|
|
1022
1397
|
WHERE singleton = true`
|
|
1398
|
+
} else if (version === 6 && marker.checksum === SCHEMA_V6_CHECKSUM) {
|
|
1399
|
+
// v6 → v9: the guarded digest-scan index above is the v6→v7 delta;
|
|
1400
|
+
// every v6 table and column already exists. Promote the marker in
|
|
1401
|
+
// this transaction.
|
|
1402
|
+
await tx`UPDATE ab_schema_migrations
|
|
1403
|
+
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1404
|
+
applied_at = ${new Date().toISOString()}
|
|
1405
|
+
WHERE singleton = true`
|
|
1406
|
+
} else if (version === 7 && marker.checksum === SCHEMA_V7_CHECKSUM) {
|
|
1407
|
+
// v7 → v9: the guarded repository-journal state-read index above
|
|
1408
|
+
// is the entire delta; every v7 table and column already exists.
|
|
1409
|
+
// Promote the marker in this transaction.
|
|
1410
|
+
await tx`UPDATE ab_schema_migrations
|
|
1411
|
+
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1412
|
+
applied_at = ${new Date().toISOString()}
|
|
1413
|
+
WHERE singleton = true`
|
|
1414
|
+
} else if (version === 8 && marker.checksum === SCHEMA_V8_CHECKSUM) {
|
|
1415
|
+
// v8 → v9: the idempotent full DDL above created the ticket_assets
|
|
1416
|
+
// table, which is the entire delta (no pre-existing table changes).
|
|
1417
|
+
// Promote the marker in this transaction.
|
|
1418
|
+
await tx`UPDATE ab_schema_migrations
|
|
1419
|
+
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1420
|
+
applied_at = ${new Date().toISOString()}
|
|
1421
|
+
WHERE singleton = true`
|
|
1023
1422
|
} else {
|
|
1024
1423
|
throw schemaError('marker is incompatible')
|
|
1025
1424
|
}
|
|
@@ -1031,11 +1430,16 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
1031
1430
|
await tx`SELECT version, checksum FROM ab_ticket_schema_migrations WHERE singleton = true FOR UPDATE`
|
|
1032
1431
|
const ticketMarker = ticketRows[0]
|
|
1033
1432
|
if (ticketMarker) {
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1433
|
+
if (Number(ticketMarker.version) !== TICKET_SCHEMA_VERSION) {
|
|
1434
|
+
throw schemaError(
|
|
1435
|
+
`ticket marker is incompatible: version ${ticketMarker.version} does not match ` +
|
|
1436
|
+
`required version ${TICKET_SCHEMA_VERSION}`,
|
|
1437
|
+
)
|
|
1438
|
+
}
|
|
1439
|
+
if (ticketMarker.checksum !== TICKET_SCHEMA_CHECKSUM) {
|
|
1440
|
+
throw schemaError(
|
|
1441
|
+
`ticket marker is incompatible: ${checksumMismatchCause(TICKET_SCHEMA_VERSION)}`,
|
|
1442
|
+
)
|
|
1039
1443
|
}
|
|
1040
1444
|
} else {
|
|
1041
1445
|
await tx`INSERT INTO ab_ticket_schema_migrations
|
|
@@ -1062,7 +1466,9 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
1062
1466
|
const authVersion = Number(authMarker.version)
|
|
1063
1467
|
if (authVersion === AUTH_SCHEMA_VERSION) {
|
|
1064
1468
|
if (authMarker.checksum !== AUTH_SCHEMA_CHECKSUM) {
|
|
1065
|
-
throw schemaError(
|
|
1469
|
+
throw schemaError(
|
|
1470
|
+
`auth marker is incompatible: ${checksumMismatchCause(AUTH_SCHEMA_VERSION)}`,
|
|
1471
|
+
)
|
|
1066
1472
|
}
|
|
1067
1473
|
} else if (authVersion === 1 && authMarker.checksum === AUTH_SCHEMA_V1_CHECKSUM) {
|
|
1068
1474
|
// v1 → v3: the idempotent full DDL above already created the four
|