@defrex/autobuild-postgres-store 0.8.0 → 0.10.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/index.ts +1 -0
- package/src/retry.ts +260 -0
- package/src/schema.ts +453 -31
- package/src/store.ts +618 -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')
|
|
@@ -495,6 +807,21 @@ export const TICKET_SCHEMA_CHECKSUM = new Bun.CryptoHasher('sha256')
|
|
|
495
807
|
.update(TICKET_SCHEMA_DDL)
|
|
496
808
|
.digest('hex')
|
|
497
809
|
|
|
810
|
+
/** Reducer snapshots (core `store/snapshots.ts`): a cache beside the event log.
|
|
811
|
+
* Deliberately outside `SCHEMA_DDL`, its marker, version and checksum, and
|
|
812
|
+
* outside `EXPECTED_COLUMNS`: a deployment at the current schema keeps serving
|
|
813
|
+
* across the upgrade, a new client against a database that lacks the table
|
|
814
|
+
* degrades to full replay (SQLSTATE 42P01 reads as "no snapshot"), and an older
|
|
815
|
+
* client never asks for it. Idempotent — `migratePostgres` creates it next to
|
|
816
|
+
* the ticket tables. */
|
|
817
|
+
export const SNAPSHOT_SCHEMA_DDL = `
|
|
818
|
+
CREATE TABLE IF NOT EXISTS reducer_snapshots (
|
|
819
|
+
scope_kind text NOT NULL, scope_key text NOT NULL, reducer text NOT NULL,
|
|
820
|
+
version integer NOT NULL, cursor bigint NOT NULL, state jsonb NOT NULL,
|
|
821
|
+
updated_at timestamptz NOT NULL,
|
|
822
|
+
PRIMARY KEY (scope_kind, scope_key, reducer)
|
|
823
|
+
);`.trim()
|
|
824
|
+
|
|
498
825
|
type ExpectedColumn = readonly [name: string, type: string, notNull: boolean, defaultValue?: string]
|
|
499
826
|
|
|
500
827
|
const EXPECTED_COLUMNS: Record<string, readonly ExpectedColumn[]> = {
|
|
@@ -536,6 +863,18 @@ const EXPECTED_COLUMNS: Record<string, readonly ExpectedColumn[]> = {
|
|
|
536
863
|
['metadata', 'jsonb', true],
|
|
537
864
|
['created_at', 'timestamp with time zone', true],
|
|
538
865
|
],
|
|
866
|
+
ticket_assets: [
|
|
867
|
+
['repo', 'text', true],
|
|
868
|
+
['ticket_id', 'text', true],
|
|
869
|
+
['kind', 'text', true],
|
|
870
|
+
['name', 'text', true],
|
|
871
|
+
['revision', 'bigint', true],
|
|
872
|
+
['layout', 'text', true],
|
|
873
|
+
['manifest', 'jsonb', true],
|
|
874
|
+
['size', 'bigint', true],
|
|
875
|
+
['removed', 'boolean', true],
|
|
876
|
+
['created_at', 'timestamp with time zone', true],
|
|
877
|
+
],
|
|
539
878
|
repo_streams: [
|
|
540
879
|
['repo', 'text', true],
|
|
541
880
|
['created_at', 'timestamp with time zone', true],
|
|
@@ -647,12 +986,30 @@ const EXPECTED_CONSTRAINTS = [
|
|
|
647
986
|
'streams|f|repo|repo_streams|repo|c|',
|
|
648
987
|
'streams|f|session|sessions|id|c|',
|
|
649
988
|
'streams|p|id||||',
|
|
989
|
+
'ticket_assets|p|repo,ticket_id,kind,name,revision||||',
|
|
650
990
|
] as const
|
|
651
991
|
|
|
652
992
|
export function schemaError(detail: string): Error {
|
|
653
993
|
return new Error(`PostgreSQL BuildStore schema ${detail}; run: ${MIGRATE_COMMAND}`)
|
|
654
994
|
}
|
|
655
995
|
|
|
996
|
+
/** The targeted cause text for a marker whose version matches the required
|
|
997
|
+
* version but whose checksum does not: exactly the shape a checksum-only
|
|
998
|
+
* re-pin leaves deployed databases in (AUT-548) — and also the shape of a
|
|
999
|
+
* database built by a different build of the same version, which a marker
|
|
1000
|
+
* alone cannot distinguish, so both causes are named. The guard's failure
|
|
1001
|
+
* output teaches the four-step rule pre-ship; this diagnostic catches the
|
|
1002
|
+
* checksum-only shortcut after the fact. */
|
|
1003
|
+
function checksumMismatchCause(version: number): string {
|
|
1004
|
+
return (
|
|
1005
|
+
`its version matches ${version}, but its checksum does not match this build's DDL — the ` +
|
|
1006
|
+
`current DDL was likely edited in place without bumping the version, or the database was ` +
|
|
1007
|
+
`built by a different build of this same version. Never edit deployed DDL; follow the ` +
|
|
1008
|
+
`four-step rule (freeze the previous DDL, add an upgrade branch in migratePostgres, bump ` +
|
|
1009
|
+
`the version, re-pin in schema-guard.test.ts) and redeploy`
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
656
1013
|
interface CatalogColumn {
|
|
657
1014
|
table_name: string
|
|
658
1015
|
column_name: string
|
|
@@ -775,7 +1132,9 @@ export async function assertSchema(sql: SQL): Promise<void> {
|
|
|
775
1132
|
if (Number(marker.version) !== SCHEMA_VERSION) {
|
|
776
1133
|
throw schemaError(`version ${marker.version} does not match required version ${SCHEMA_VERSION}`)
|
|
777
1134
|
}
|
|
778
|
-
if (marker.checksum !== SCHEMA_CHECKSUM)
|
|
1135
|
+
if (marker.checksum !== SCHEMA_CHECKSUM) {
|
|
1136
|
+
throw schemaError(`checksum is mismatched: ${checksumMismatchCause(SCHEMA_VERSION)}`)
|
|
1137
|
+
}
|
|
779
1138
|
await assertCatalogShape(sql)
|
|
780
1139
|
}
|
|
781
1140
|
|
|
@@ -792,11 +1151,16 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
|
|
|
792
1151
|
}
|
|
793
1152
|
const marker = rows[0]
|
|
794
1153
|
if (!marker) throw schemaError('ticket marker is missing')
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1154
|
+
if (Number(marker.version) !== TICKET_SCHEMA_VERSION) {
|
|
1155
|
+
throw schemaError(
|
|
1156
|
+
`ticket marker is incompatible: version ${marker.version} does not match required version ` +
|
|
1157
|
+
`${TICKET_SCHEMA_VERSION}`,
|
|
1158
|
+
)
|
|
1159
|
+
}
|
|
1160
|
+
if (marker.checksum !== TICKET_SCHEMA_CHECKSUM) {
|
|
1161
|
+
throw schemaError(
|
|
1162
|
+
`ticket marker is incompatible: ${checksumMismatchCause(TICKET_SCHEMA_VERSION)}`,
|
|
1163
|
+
)
|
|
800
1164
|
}
|
|
801
1165
|
const expected: Record<string, Array<[string, string, boolean]>> = {
|
|
802
1166
|
ab_ticket_schema_migrations: [
|
|
@@ -851,7 +1215,7 @@ export async function assertTicketSchema(sql: SQL): Promise<void> {
|
|
|
851
1215
|
}
|
|
852
1216
|
}
|
|
853
1217
|
|
|
854
|
-
/** The guarded streams.creation_seq upgrade, shared by every pre-
|
|
1218
|
+
/** The guarded streams.creation_seq upgrade, shared by every pre-v7 marker
|
|
855
1219
|
* branch whose database carries a pre-existing streams table (v2, v3, v4, v5).
|
|
856
1220
|
* The idempotent full DDL above has already created the
|
|
857
1221
|
* streams_creation_seq sequence; the column is added only when missing, with
|
|
@@ -886,17 +1250,18 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
886
1250
|
await tx`SELECT pg_advisory_xact_lock(470281941)`
|
|
887
1251
|
await tx.unsafe(SCHEMA_DDL)
|
|
888
1252
|
await tx.unsafe(TICKET_SCHEMA_DDL)
|
|
1253
|
+
await tx.unsafe(SNAPSHOT_SCHEMA_DDL)
|
|
889
1254
|
await tx.unsafe(AUTH_SCHEMA_DDL)
|
|
890
1255
|
const rows: { version: number; checksum: string }[] =
|
|
891
1256
|
await tx`SELECT version, checksum FROM ab_schema_migrations WHERE singleton = true FOR UPDATE`
|
|
892
1257
|
const marker = rows[0]
|
|
893
1258
|
if (marker) {
|
|
894
|
-
// v1–
|
|
895
|
-
// pre-
|
|
1259
|
+
// v1–v7 → v9: the guarded builds.repo_origin column runs for EVERY
|
|
1260
|
+
// pre-v7 marker, before the version branches — not only in a v4
|
|
896
1261
|
// branch. `CREATE TABLE IF NOT EXISTS builds` does not alter an
|
|
897
1262
|
// existing builds table, so without this the internal assertSchema
|
|
898
1263
|
// below would fail on every upgraded legacy database. The ALTER is
|
|
899
|
-
// guarded and idempotent, so re-running it on a
|
|
1264
|
+
// guarded and idempotent, so re-running it on a v7 database (or a
|
|
900
1265
|
// marker that turns out to be rejected below, which rolls this
|
|
901
1266
|
// transaction back) is a no-op.
|
|
902
1267
|
await tx.unsafe(`
|
|
@@ -910,25 +1275,49 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
910
1275
|
END IF;
|
|
911
1276
|
END $$;
|
|
912
1277
|
`)
|
|
1278
|
+
// v6 → v7: the guarded build-digest scan index (AUT-487) likewise
|
|
1279
|
+
// runs for EVERY pre-v7 marker (and, with the state-read index below,
|
|
1280
|
+
// every pre-v8 marker) — the idempotent full DDL above only
|
|
1281
|
+
// creates an index on a database that lacks the events table, never
|
|
1282
|
+
// on a pre-existing one. Guarded and idempotent (the repo_origin
|
|
1283
|
+
// precedent), so it is a no-op on fresh installs, on v7 reruns, and
|
|
1284
|
+
// on markers rejected below (which roll the transaction back).
|
|
1285
|
+
await tx.unsafe(
|
|
1286
|
+
`CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq)`,
|
|
1287
|
+
)
|
|
1288
|
+
// v7 → v9: the guarded repository-journal state-read index (AUT-489)
|
|
1289
|
+
// likewise runs for EVERY pre-v8 marker — same reasoning as the
|
|
1290
|
+
// digest-scan index above. Guarded and idempotent, so it is a no-op
|
|
1291
|
+
// on fresh installs, on v8 reruns, and on markers rejected below
|
|
1292
|
+
// (which roll the transaction back). Indexes are not catalog-asserted,
|
|
1293
|
+
// so no EXPECTED_* change.
|
|
1294
|
+
await tx.unsafe(
|
|
1295
|
+
`CREATE INDEX IF NOT EXISTS repo_events_type_repo_seq ON repo_events (repo, type, seq)`,
|
|
1296
|
+
)
|
|
913
1297
|
const version = Number(marker.version)
|
|
914
1298
|
if (version === SCHEMA_VERSION) {
|
|
915
|
-
if (marker.checksum !== SCHEMA_CHECKSUM)
|
|
1299
|
+
if (marker.checksum !== SCHEMA_CHECKSUM) {
|
|
1300
|
+
throw schemaError(`marker is incompatible: ${checksumMismatchCause(SCHEMA_VERSION)}`)
|
|
1301
|
+
}
|
|
916
1302
|
} else if (version === 1 && marker.checksum === SCHEMA_V1_CHECKSUM) {
|
|
917
|
-
// v1 →
|
|
1303
|
+
// v1 → v9: the idempotent full DDL above already applied the deltas
|
|
918
1304
|
// (the stream tables and the session tables); v1 databases never had
|
|
919
1305
|
// a streams or sessions table, so the full DDL created them with the
|
|
920
1306
|
// session column, the widened CHECKs, and both creation_seq columns.
|
|
921
|
-
// The guarded repo_origin ALTER
|
|
922
|
-
// marker in this
|
|
1307
|
+
// The guarded repo_origin ALTER and the guarded digest-scan index
|
|
1308
|
+
// above covered the pre-existing tables. Promote the marker in this
|
|
1309
|
+
// transaction.
|
|
923
1310
|
await tx`UPDATE ab_schema_migrations
|
|
924
1311
|
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
925
1312
|
applied_at = ${new Date().toISOString()}
|
|
926
1313
|
WHERE singleton = true`
|
|
927
1314
|
} else if (version === 2 && marker.checksum === SCHEMA_V2_CHECKSUM) {
|
|
928
|
-
// v2 →
|
|
1315
|
+
// v2 → v9: the idempotent full DDL above created the session tables
|
|
929
1316
|
// (with the creation_seq column); a v2 database's streams table
|
|
930
1317
|
// needs the guarded session column, the widened CHECK constraints,
|
|
931
|
-
// and the guarded creation_seq column (shared with v3 and v4).
|
|
1318
|
+
// and the guarded creation_seq column (shared with v3 and v4). The
|
|
1319
|
+
// guarded repo_origin ALTER and the guarded digest-scan index above
|
|
1320
|
+
// covered the pre-existing builds and events tables.
|
|
932
1321
|
await tx.unsafe(`
|
|
933
1322
|
DO $$ BEGIN
|
|
934
1323
|
IF NOT EXISTS (
|
|
@@ -958,7 +1347,7 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
958
1347
|
applied_at = ${new Date().toISOString()}
|
|
959
1348
|
WHERE singleton = true`
|
|
960
1349
|
} else if (version === 3 && marker.checksum === SCHEMA_V3_CHECKSUM) {
|
|
961
|
-
// v3 →
|
|
1350
|
+
// v3 → v9: the listSessions and listStreams creation-order
|
|
962
1351
|
// tiebreaks (store/types.ts). The idempotent full DDL above created
|
|
963
1352
|
// the sessions_creation_seq and streams_creation_seq sequences; a
|
|
964
1353
|
// v3 database's sessions and streams tables need the guarded
|
|
@@ -967,8 +1356,8 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
967
1356
|
// unorderable, so any total order consistent with createdAt is
|
|
968
1357
|
// acceptable — SET NOT NULL, and the sequence positioned above the
|
|
969
1358
|
// backfilled values so the next nextval continues the counter
|
|
970
|
-
// without collision. The guarded repo_origin ALTER
|
|
971
|
-
//
|
|
1359
|
+
// without collision. The guarded repo_origin ALTER and the guarded
|
|
1360
|
+
// digest-scan index above covered the pre-existing tables.
|
|
972
1361
|
await tx.unsafe(`
|
|
973
1362
|
DO $$ BEGIN
|
|
974
1363
|
IF NOT EXISTS (
|
|
@@ -994,32 +1383,58 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
994
1383
|
applied_at = ${new Date().toISOString()}
|
|
995
1384
|
WHERE singleton = true`
|
|
996
1385
|
} else if (version === 4 && marker.checksum === SCHEMA_V4_CHECKSUM) {
|
|
997
|
-
// v4 →
|
|
1386
|
+
// v4 → v9: the listStreams creation-order tiebreak
|
|
998
1387
|
// (store/types.ts), mirroring the v3→v4 sessions treatment. The
|
|
999
1388
|
// idempotent full DDL above created the streams_creation_seq
|
|
1000
1389
|
// sequence; a v4 database's streams table needs the guarded
|
|
1001
1390
|
// creation_seq column, backfill, NOT NULL, and sequence continuity
|
|
1002
1391
|
// (the shared pre-v6 streams migration). The guarded repo_origin
|
|
1003
|
-
// ALTER above covered
|
|
1392
|
+
// ALTER and the guarded digest-scan index above covered the
|
|
1393
|
+
// pre-existing tables.
|
|
1004
1394
|
await tx.unsafe(STREAMS_CREATION_SEQ_MIGRATION)
|
|
1005
1395
|
await tx`UPDATE ab_schema_migrations
|
|
1006
1396
|
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1007
1397
|
applied_at = ${new Date().toISOString()}
|
|
1008
1398
|
WHERE singleton = true`
|
|
1009
1399
|
} else if (version === 5 && marker.checksum === SCHEMA_V5_CHECKSUM) {
|
|
1010
|
-
// v5 →
|
|
1400
|
+
// v5 → v9: the listStreams creation-order tiebreak (store/types.ts)
|
|
1011
1401
|
// landed on the v5 DDL without a version bump, so the hosted
|
|
1012
1402
|
// database carries the v5 marker with the pre-tiebreak checksum. The
|
|
1013
1403
|
// idempotent full DDL above created the streams_creation_seq
|
|
1014
1404
|
// sequence; a v5 database's streams table needs the guarded
|
|
1015
1405
|
// 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
|
|
1406
|
+
// (the shared pre-v6 streams migration). Every other pre-v7 table
|
|
1407
|
+
// and column already exists in a v5 database; the guarded
|
|
1408
|
+
// digest-scan index above covers events.
|
|
1018
1409
|
await tx.unsafe(STREAMS_CREATION_SEQ_MIGRATION)
|
|
1019
1410
|
await tx`UPDATE ab_schema_migrations
|
|
1020
1411
|
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1021
1412
|
applied_at = ${new Date().toISOString()}
|
|
1022
1413
|
WHERE singleton = true`
|
|
1414
|
+
} else if (version === 6 && marker.checksum === SCHEMA_V6_CHECKSUM) {
|
|
1415
|
+
// v6 → v9: the guarded digest-scan index above is the v6→v7 delta;
|
|
1416
|
+
// every v6 table and column already exists. Promote the marker in
|
|
1417
|
+
// 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`
|
|
1422
|
+
} else if (version === 7 && marker.checksum === SCHEMA_V7_CHECKSUM) {
|
|
1423
|
+
// v7 → v9: the guarded repository-journal state-read index above
|
|
1424
|
+
// is the entire delta; every v7 table and column already exists.
|
|
1425
|
+
// Promote the marker in this transaction.
|
|
1426
|
+
await tx`UPDATE ab_schema_migrations
|
|
1427
|
+
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1428
|
+
applied_at = ${new Date().toISOString()}
|
|
1429
|
+
WHERE singleton = true`
|
|
1430
|
+
} else if (version === 8 && marker.checksum === SCHEMA_V8_CHECKSUM) {
|
|
1431
|
+
// v8 → v9: the idempotent full DDL above created the ticket_assets
|
|
1432
|
+
// table, which is the entire delta (no pre-existing table changes).
|
|
1433
|
+
// Promote the marker in this transaction.
|
|
1434
|
+
await tx`UPDATE ab_schema_migrations
|
|
1435
|
+
SET version = ${SCHEMA_VERSION}, checksum = ${SCHEMA_CHECKSUM},
|
|
1436
|
+
applied_at = ${new Date().toISOString()}
|
|
1437
|
+
WHERE singleton = true`
|
|
1023
1438
|
} else {
|
|
1024
1439
|
throw schemaError('marker is incompatible')
|
|
1025
1440
|
}
|
|
@@ -1031,11 +1446,16 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
1031
1446
|
await tx`SELECT version, checksum FROM ab_ticket_schema_migrations WHERE singleton = true FOR UPDATE`
|
|
1032
1447
|
const ticketMarker = ticketRows[0]
|
|
1033
1448
|
if (ticketMarker) {
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1449
|
+
if (Number(ticketMarker.version) !== TICKET_SCHEMA_VERSION) {
|
|
1450
|
+
throw schemaError(
|
|
1451
|
+
`ticket marker is incompatible: version ${ticketMarker.version} does not match ` +
|
|
1452
|
+
`required version ${TICKET_SCHEMA_VERSION}`,
|
|
1453
|
+
)
|
|
1454
|
+
}
|
|
1455
|
+
if (ticketMarker.checksum !== TICKET_SCHEMA_CHECKSUM) {
|
|
1456
|
+
throw schemaError(
|
|
1457
|
+
`ticket marker is incompatible: ${checksumMismatchCause(TICKET_SCHEMA_VERSION)}`,
|
|
1458
|
+
)
|
|
1039
1459
|
}
|
|
1040
1460
|
} else {
|
|
1041
1461
|
await tx`INSERT INTO ab_ticket_schema_migrations
|
|
@@ -1062,7 +1482,9 @@ export async function migratePostgres(url: string): Promise<void> {
|
|
|
1062
1482
|
const authVersion = Number(authMarker.version)
|
|
1063
1483
|
if (authVersion === AUTH_SCHEMA_VERSION) {
|
|
1064
1484
|
if (authMarker.checksum !== AUTH_SCHEMA_CHECKSUM) {
|
|
1065
|
-
throw schemaError(
|
|
1485
|
+
throw schemaError(
|
|
1486
|
+
`auth marker is incompatible: ${checksumMismatchCause(AUTH_SCHEMA_VERSION)}`,
|
|
1487
|
+
)
|
|
1066
1488
|
}
|
|
1067
1489
|
} else if (authVersion === 1 && authMarker.checksum === AUTH_SCHEMA_V1_CHECKSUM) {
|
|
1068
1490
|
// v1 → v3: the idempotent full DDL above already created the four
|