d24e402ef6
jobs.idempotency_key is globally unique, but test-poll:<pub>:<seq> and test-validate:<pub>:<seq> were reused verbatim by every manual retry. The first round's completed rows made ON CONFLICT DO NOTHING silently swallow the next round's schedule SQL: a retried publication re-uploaded, polled once, and then froze in pending/uploaded with no successor job and no error. Keys and job ids are now derived from (publication, retry_count, sequence) inside the schedule SQL so every retry round owns fresh orchestration rows.
602 lines
24 KiB
Go
602 lines
24 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
func TestLoadPublicationForUploadMapsVerifiedCandidate(t *testing.T) {
|
|
db := &recordingDB{row: publicationUploadRow{}}
|
|
candidate, err := NewStore(db).LoadPublicationForUpload(context.Background(), "publication-1")
|
|
if err != nil {
|
|
t.Fatalf("LoadPublicationForUpload() error = %v", err)
|
|
}
|
|
if candidate.ID != "publication-1" || candidate.SourceItemVersionID != "version-1" || candidate.ContentObjectID != "content-1" {
|
|
t.Fatalf("candidate identities = %#v", candidate)
|
|
}
|
|
if candidate.TestKBID != "kb-1" || candidate.IngestState != governance.IngestPending || candidate.ProcessingState != governance.ProcessingPending {
|
|
t.Fatalf("candidate states = %#v", candidate)
|
|
}
|
|
if candidate.TargetScope != "internal" {
|
|
t.Fatalf("candidate target scope = %q", candidate.TargetScope)
|
|
}
|
|
if candidate.SHA256 != "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" || candidate.SizeBytes != 24 || candidate.ValidatedFormat != "md" || candidate.LocalSpoolRef != "objects/artifact.md" {
|
|
t.Fatalf("candidate content = %#v", candidate)
|
|
}
|
|
if db.queryRowSQL != loadPublicationForUploadSQL || len(db.queryRowArgs) != 1 || db.queryRowArgs[0] != "publication-1" {
|
|
t.Fatalf("query = %q %#v", db.queryRowSQL, db.queryRowArgs)
|
|
}
|
|
}
|
|
|
|
func TestMarkUploadingRejectsStalePublicationState(t *testing.T) {
|
|
db := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 0")}
|
|
err := NewStore(db).MarkUploading(context.Background(), "publication-1", time.Date(2026, time.September, 4, 2, 0, 0, 0, time.UTC))
|
|
if !errors.Is(err, ErrPublicationStateConflict) {
|
|
t.Fatalf("MarkUploading() error = %v, want ErrPublicationStateConflict", err)
|
|
}
|
|
if db.execSQL != markUploadingSQL {
|
|
t.Fatalf("MarkUploading() SQL = %q", db.execSQL)
|
|
}
|
|
}
|
|
|
|
func TestRecordUploadAndSchedulePollIsAtomicAndIdempotent(t *testing.T) {
|
|
db := &recordingDB{row: sequenceRow{sequence: 1}}
|
|
now := time.Date(2026, time.September, 4, 2, 5, 0, 0, time.UTC)
|
|
nextAt := now.Add(10 * time.Second)
|
|
sequence, err := NewStore(db).RecordUploadAndSchedulePoll(context.Background(), "publication-1", "knowledge-1", now, nextAt)
|
|
if err != nil {
|
|
t.Fatalf("RecordUploadAndSchedulePoll() error = %v", err)
|
|
}
|
|
if sequence != 1 {
|
|
t.Fatalf("sequence = %d, want 1", sequence)
|
|
}
|
|
updateAt := strings.Index(recordUploadAndSchedulePollSQL, "UPDATE test_publications")
|
|
insertAt := strings.Index(recordUploadAndSchedulePollSQL, "INSERT INTO jobs")
|
|
if updateAt < 0 || insertAt < 0 || updateAt >= insertAt {
|
|
t.Fatalf("atomic SQL missing ordered update/insert: %q", recordUploadAndSchedulePollSQL)
|
|
}
|
|
if db.queryRowSQL != recordUploadAndSchedulePollSQL || len(db.queryRowArgs) != 7 {
|
|
t.Fatalf("query = %q %#v", db.queryRowSQL, db.queryRowArgs)
|
|
}
|
|
if !strings.Contains(recordUploadAndSchedulePollSQL, "'test-poll:' || $1 || ':' || updated.retry_count || ':1'") {
|
|
t.Fatalf("poll schedule key must carry the retry round: %q", recordUploadAndSchedulePollSQL)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(db.queryRowArgs[3].([]byte), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(payload) != 2 || payload["publication_id"] != "publication-1" || payload["sequence"] != float64(1) {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestRecordRemoteStatusSchedulesNextSequenceAtomically(t *testing.T) {
|
|
db := &recordingDB{row: sequenceRow{sequence: 2}}
|
|
observedAt := time.Date(2026, time.September, 4, 2, 6, 0, 0, time.UTC)
|
|
remoteUpdatedAt := observedAt.Add(-time.Second)
|
|
nextAt := observedAt.Add(30 * time.Second)
|
|
sequence, err := NewStore(db).RecordRemoteStatusAndScheduleNextPoll(
|
|
context.Background(),
|
|
"publication-1",
|
|
1,
|
|
governance.ProcessingProcessing,
|
|
&remoteUpdatedAt,
|
|
observedAt,
|
|
nextAt,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("RecordRemoteStatusAndScheduleNextPoll() error = %v", err)
|
|
}
|
|
if sequence != 2 {
|
|
t.Fatalf("sequence = %d, want 2", sequence)
|
|
}
|
|
if !strings.Contains(recordRemoteStatusAndScheduleNextPollSQL, "poll_sequence = $2") || !strings.Contains(recordRemoteStatusAndScheduleNextPollSQL, "INSERT INTO jobs") {
|
|
t.Fatalf("SQL lacks sequence guard or atomic enqueue: %q", recordRemoteStatusAndScheduleNextPollSQL)
|
|
}
|
|
if db.queryRowSQL != recordRemoteStatusAndScheduleNextPollSQL || len(db.queryRowArgs) != 9 {
|
|
t.Fatalf("query = %q %#v", db.queryRowSQL, db.queryRowArgs)
|
|
}
|
|
if !strings.Contains(recordRemoteStatusAndScheduleNextPollSQL, "'test-poll:' || $1 || ':' || updated.retry_count || ':' || updated.poll_sequence") {
|
|
t.Fatalf("next-poll schedule key must carry the retry round: %q", recordRemoteStatusAndScheduleNextPollSQL)
|
|
}
|
|
}
|
|
|
|
func TestRecordCompletedSchedulesValidationAtomically(t *testing.T) {
|
|
db := &recordingDB{row: sequenceRow{sequence: 3}}
|
|
observedAt := time.Date(2026, time.September, 4, 2, 7, 0, 0, time.UTC)
|
|
remoteUpdatedAt := observedAt.Add(-time.Second)
|
|
err := NewStore(db).RecordCompletedAndScheduleValidation(
|
|
context.Background(), "publication-1", 3, &remoteUpdatedAt, observedAt,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("RecordCompletedAndScheduleValidation() error = %v", err)
|
|
}
|
|
if !strings.Contains(recordCompletedAndScheduleValidationSQL, "processing_state = 'completed'") || !strings.Contains(recordCompletedAndScheduleValidationSQL, "'test.validate'") {
|
|
t.Fatalf("SQL lacks completed update or validation enqueue: %q", recordCompletedAndScheduleValidationSQL)
|
|
}
|
|
if !strings.Contains(recordCompletedAndScheduleValidationSQL, "parser_generation = $7") || !strings.Contains(recordCompletedAndScheduleValidationSQL, "index_generation = $8") {
|
|
t.Fatalf("SQL lacks release generation stamping: %q", recordCompletedAndScheduleValidationSQL)
|
|
}
|
|
if db.queryRowSQL != recordCompletedAndScheduleValidationSQL || len(db.queryRowArgs) != 10 {
|
|
t.Fatalf("query = %q %#v", db.queryRowSQL, db.queryRowArgs)
|
|
}
|
|
if db.queryRowArgs[6] != "weknora-v0.7.1-m74@2026-09-04T02:06:59Z" || db.queryRowArgs[7] != "weknora-v0.7.1-m74@2026-09-04T02:06:59Z" {
|
|
t.Fatalf("generation args = %#v", db.queryRowArgs[6:])
|
|
}
|
|
if !strings.Contains(recordCompletedAndScheduleValidationSQL, "'test-validate:' || $1 || ':' || updated.retry_count || ':' || updated.poll_sequence") {
|
|
t.Fatalf("validate schedule key must carry the retry round: %q", recordCompletedAndScheduleValidationSQL)
|
|
}
|
|
}
|
|
|
|
func TestLoadPublicationForPollMapsOnlyProcessingProjection(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 2, 8, 0, 0, time.UTC)
|
|
db := &recordingDB{row: publicationPollRow{now: now}}
|
|
publication, err := NewStore(db).LoadPublicationForPoll(context.Background(), "publication-1")
|
|
if err != nil {
|
|
t.Fatalf("LoadPublicationForPoll() error = %v", err)
|
|
}
|
|
if publication.ID != "publication-1" || publication.TestKBID != "kb-1" || publication.KnowledgeID != "knowledge-1" {
|
|
t.Fatalf("publication identities = %#v", publication)
|
|
}
|
|
if publication.ProcessingState != governance.ProcessingProcessing || publication.PollSequence != 2 || publication.LastRemoteStatus != "processing" {
|
|
t.Fatalf("publication state = %#v", publication)
|
|
}
|
|
if publication.LastProgressAt == nil || !publication.LastProgressAt.Equal(now) {
|
|
t.Fatalf("last progress = %#v", publication.LastProgressAt)
|
|
}
|
|
if db.queryRowSQL != loadPublicationForPollSQL {
|
|
t.Fatalf("query = %q", db.queryRowSQL)
|
|
}
|
|
}
|
|
|
|
func TestRecordProcessingFailureStoresOnlyFixedErrorCode(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 2, 9, 0, 0, time.UTC)
|
|
for _, code := range []string{"remote detail", "https://example.com", strings.Repeat("x", 65)} {
|
|
db := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 1")}
|
|
if err := NewStore(db).RecordProcessingFailure(context.Background(), "publication-1", 2, code, now); !errors.Is(err, ErrInvalidErrorCode) {
|
|
t.Fatalf("RecordProcessingFailure(%q) error = %v", code, err)
|
|
}
|
|
if db.execSQL != "" {
|
|
t.Fatalf("invalid code %q reached database", code)
|
|
}
|
|
}
|
|
db := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 1")}
|
|
if err := NewStore(db).RecordProcessingFailure(context.Background(), "publication-1", 2, "remote_parse_failed", now); err != nil {
|
|
t.Fatalf("RecordProcessingFailure() error = %v", err)
|
|
}
|
|
if db.execSQL != recordProcessingFailureSQL || db.execArgs[2] != "remote_parse_failed" {
|
|
t.Fatalf("failure update = %q %#v", db.execSQL, db.execArgs)
|
|
}
|
|
}
|
|
|
|
func TestRecordUploadFailureStoresOnlyFixedErrorCode(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 2, 9, 30, 0, time.UTC)
|
|
db := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 1")}
|
|
if err := NewStore(db).RecordUploadFailure(context.Background(), "publication-1", "upload_identity_ambiguous", now); err != nil {
|
|
t.Fatalf("RecordUploadFailure() error = %v", err)
|
|
}
|
|
if db.execSQL != recordUploadFailureSQL || db.execArgs[1] != "upload_identity_ambiguous" {
|
|
t.Fatalf("recorded upload failure args = %#v", db.execArgs)
|
|
}
|
|
if !strings.Contains(recordUploadFailureSQL, "processing_state = 'failed'") {
|
|
t.Fatal("upload terminal failure must enter retryable processing_state=failed")
|
|
}
|
|
invalidDB := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 1")}
|
|
if err := NewStore(invalidDB).RecordUploadFailure(context.Background(), "publication-1", "remote body: token=secret", now); !errors.Is(err, ErrInvalidErrorCode) {
|
|
t.Fatalf("invalid error code result = %v", err)
|
|
}
|
|
if invalidDB.execSQL != "" {
|
|
t.Fatal("invalid error code reached database")
|
|
}
|
|
}
|
|
|
|
func TestLoadPublicationForValidationMapsCompletedContext(t *testing.T) {
|
|
db := &recordingDB{row: publicationValidationRow{}}
|
|
context, err := NewStore(db).LoadPublicationForValidation(context.Background(), "publication-1")
|
|
if err != nil {
|
|
t.Fatalf("LoadPublicationForValidation() error = %v", err)
|
|
}
|
|
if context.ID != "publication-1" || context.TestKBID != "kb-1" || context.KnowledgeID != "knowledge-1" {
|
|
t.Fatalf("validation context = %#v", context)
|
|
}
|
|
if db.queryRowSQL != loadPublicationForValidationSQL || len(db.queryRowArgs) != 1 || db.queryRowArgs[0] != "publication-1" {
|
|
t.Fatalf("query = %q %#v", db.queryRowSQL, db.queryRowArgs)
|
|
}
|
|
}
|
|
|
|
func TestListLegacyVerificationCandidatesMapsKnowledgeIDs(t *testing.T) {
|
|
rows := &legacyVerificationRows{items: []legacyVerificationRow{
|
|
{id: "pub-1", kbID: "kb-1", knowledgeID: "knowledge-1"},
|
|
{id: "pub-2", kbID: "kb-2", knowledgeID: "knowledge-2"},
|
|
}}
|
|
db := &recordingDB{rows: rows}
|
|
candidates, err := NewStore(db).ListLegacyVerificationCandidates(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ListLegacyVerificationCandidates() error = %v", err)
|
|
}
|
|
if len(candidates) != 2 || candidates[0].ID != "pub-1" || candidates[0].KnowledgeID != "knowledge-1" || candidates[1].TestKBID != "kb-2" {
|
|
t.Fatalf("candidates = %#v", candidates)
|
|
}
|
|
if db.querySQL != listLegacyVerificationCandidatesSQL {
|
|
t.Fatalf("query = %q", db.querySQL)
|
|
}
|
|
if !strings.Contains(listLegacyVerificationCandidatesSQL, "source_type = 'pp'") {
|
|
t.Fatal("legacy verification candidates must exclude manual and future source types")
|
|
}
|
|
}
|
|
|
|
func TestListTestPublicationsReturnsSafeProjection(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 5, 0, 0, 0, time.UTC)
|
|
rows := &testPublicationRows{items: []testPublicationRow{
|
|
{id: "pub-1", title: "Safe title", ingest: "uploaded", processing: string(governance.ProcessingFailed),
|
|
retrieval: "unpublished", targetScope: "external", changeSetID: "cs-1", changeType: "updated", canStart: false, retryCount: 2, lastError: "remote_parse_failed", canRetry: true, updated: now},
|
|
}}
|
|
db := &recordingDB{rows: rows, row: totalRow{total: 1}}
|
|
filter := governance.TestPublicationFilter{ProcessingState: string(governance.ProcessingFailed), Limit: 25, Offset: 0}
|
|
page, err := NewStore(db).ListTestPublications(context.Background(), filter)
|
|
if err != nil {
|
|
t.Fatalf("ListTestPublications() error = %v", err)
|
|
}
|
|
if page.Total != 1 || len(page.Items) != 1 {
|
|
t.Fatalf("page = %#v", page)
|
|
}
|
|
item := page.Items[0]
|
|
if item.ID != "pub-1" || item.Title != "Safe title" || item.TargetScope != "external" || item.ChangeSetID != "cs-1" || item.ChangeType != "updated" || item.CanStart || item.ProcessingState != string(governance.ProcessingFailed) || item.RetryCount != 2 || item.LastError != "remote_parse_failed" || !item.CanRetry {
|
|
t.Fatalf("item = %#v", item)
|
|
}
|
|
if db.queryRowSQL != countTestPublicationsSQL || db.querySQL != listTestPublicationsSQL {
|
|
t.Fatalf("count = %q list = %q", db.queryRowSQL, db.querySQL)
|
|
}
|
|
}
|
|
|
|
func TestListAcceptanceResultsMapsSafeProjection(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 5, 5, 0, 0, time.UTC)
|
|
rows := &acceptanceRecordRows{items: []acceptanceRecordRow{
|
|
{checkType: "chunks_present", state: "passed", policy: "structural-v1", evidence: json.RawMessage(`{"chunk_total":2}`), observed: now},
|
|
}}
|
|
db := &recordingDB{rows: rows}
|
|
results, err := NewStore(db).ListAcceptanceResults(context.Background(), "publication-1")
|
|
if err != nil {
|
|
t.Fatalf("ListAcceptanceResults() error = %v", err)
|
|
}
|
|
if len(results) != 1 || results[0].CheckType != "chunks_present" || results[0].State != "passed" || string(results[0].EvidenceSummary) != `{"chunk_total":2}` {
|
|
t.Fatalf("results = %#v", results)
|
|
}
|
|
if db.querySQL != listAcceptanceResultsSQL || len(db.queryArgs) != 1 || db.queryArgs[0] != "publication-1" {
|
|
t.Fatalf("query = %q %#v", db.querySQL, db.queryArgs)
|
|
}
|
|
}
|
|
|
|
func TestRetryPublicationResetsAndEnqueuesUploadJob(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 5, 10, 0, 0, time.UTC)
|
|
db := &recordingDB{
|
|
row: retryStateRow{processing: "failed", ingest: "uploaded", spoolAvailable: true},
|
|
commandTag: pgconn.NewCommandTag("INSERT 0 1"),
|
|
}
|
|
if err := NewStore(db).RetryPublication(context.Background(), "publication-1", now); err != nil {
|
|
t.Fatalf("RetryPublication() error = %v", err)
|
|
}
|
|
if db.queryRowSQL != loadRetryStateSQL || db.execSQL != retryPublicationSQL {
|
|
t.Fatalf("query = %q exec = %q", db.queryRowSQL, db.execSQL)
|
|
}
|
|
if len(db.execArgs) != 4 || db.execArgs[0] != "publication-1" {
|
|
t.Fatalf("exec args = %#v", db.execArgs)
|
|
}
|
|
}
|
|
|
|
func TestRetryPublicationRecoversLegacyUploadFailedState(t *testing.T) {
|
|
now := time.Date(2026, time.September, 5, 5, 30, 0, 0, time.UTC)
|
|
db := &recordingDB{
|
|
row: retryStateRow{processing: "pending", ingest: "failed", spoolAvailable: true},
|
|
commandTag: pgconn.NewCommandTag("INSERT 0 1"),
|
|
}
|
|
if err := NewStore(db).RetryPublication(context.Background(), "publication-1", now); err != nil {
|
|
t.Fatalf("RetryPublication() legacy upload failure error = %v", err)
|
|
}
|
|
if !strings.Contains(retryPublicationSQL, "ingest_state = 'failed'") {
|
|
t.Fatal("retry SQL must recover legacy upload-failed state")
|
|
}
|
|
}
|
|
|
|
func TestRetryPublicationRejectsCompleted(t *testing.T) {
|
|
db := &recordingDB{row: retryStateRow{processing: "completed", ingest: "uploaded", spoolAvailable: true}}
|
|
err := NewStore(db).RetryPublication(context.Background(), "publication-1", time.Date(2026, time.September, 4, 5, 11, 0, 0, time.UTC))
|
|
if !errors.Is(err, ErrPublicationStateConflict) {
|
|
t.Fatalf("RetryPublication() error = %v, want state conflict", err)
|
|
}
|
|
if db.execSQL != "" {
|
|
t.Fatal("completed publication reached the retry UPDATE")
|
|
}
|
|
}
|
|
|
|
func TestRetryPublicationRejectsMissingSpool(t *testing.T) {
|
|
db := &recordingDB{row: retryStateRow{processing: "failed", ingest: "uploaded", spoolAvailable: false}}
|
|
err := NewStore(db).RetryPublication(context.Background(), "publication-1", time.Date(2026, time.September, 4, 5, 12, 0, 0, time.UTC))
|
|
if !errors.Is(err, ErrPublicationNotRetryable) {
|
|
t.Fatalf("RetryPublication() error = %v, want not retryable", err)
|
|
}
|
|
}
|
|
|
|
func TestMarkStalledRechecksNoProgressCutoffInDatabase(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 2, 10, 0, 0, time.UTC)
|
|
cutoff := now.Add(-30 * time.Minute)
|
|
db := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 1")}
|
|
if err := NewStore(db).MarkStalled(context.Background(), "publication-1", 3, cutoff, now); err != nil {
|
|
t.Fatalf("MarkStalled() error = %v", err)
|
|
}
|
|
if db.execSQL != markStalledSQL || !strings.Contains(markStalledSQL, "updated_at <= $3") {
|
|
t.Fatalf("stalled SQL = %q", db.execSQL)
|
|
}
|
|
staleDB := &recordingDB{commandTag: pgconn.NewCommandTag("UPDATE 0")}
|
|
if err := NewStore(staleDB).MarkStalled(context.Background(), "publication-1", 3, cutoff, now); !errors.Is(err, ErrPublicationStateConflict) {
|
|
t.Fatalf("MarkStalled() stale error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAppendAcceptanceResultValidatesAndAppendsEvidence(t *testing.T) {
|
|
now := time.Date(2026, time.September, 4, 2, 11, 0, 0, time.UTC)
|
|
result := governance.AcceptanceResult{
|
|
ID: "acceptance-1",
|
|
PublicationID: "publication-1",
|
|
RunID: "run-1",
|
|
CheckType: "chunk_structure",
|
|
State: governance.AcceptancePassed,
|
|
PolicyVersion: "structural-v1",
|
|
EvidenceSummary: json.RawMessage(`{"chunk_total":2,"parent_match":true}`),
|
|
ObservedAt: now,
|
|
}
|
|
db := &recordingDB{commandTag: pgconn.NewCommandTag("INSERT 0 1")}
|
|
if err := NewStore(db).AppendAcceptanceResult(context.Background(), result); err != nil {
|
|
t.Fatalf("AppendAcceptanceResult() error = %v", err)
|
|
}
|
|
if db.execSQL != appendAcceptanceResultSQL || len(db.execArgs) != 8 {
|
|
t.Fatalf("acceptance insert = %q %#v", db.execSQL, db.execArgs)
|
|
}
|
|
|
|
result.EvidenceSummary = json.RawMessage(`"raw text"`)
|
|
invalidDB := &recordingDB{commandTag: pgconn.NewCommandTag("INSERT 0 1")}
|
|
if err := NewStore(invalidDB).AppendAcceptanceResult(context.Background(), result); !errors.Is(err, ErrInvalidAcceptanceResult) {
|
|
t.Fatalf("invalid acceptance error = %v", err)
|
|
}
|
|
if invalidDB.execSQL != "" {
|
|
t.Fatal("invalid acceptance result reached database")
|
|
}
|
|
}
|
|
|
|
type publicationUploadRow struct{}
|
|
|
|
type publicationValidationRow struct{}
|
|
|
|
func (publicationValidationRow) Scan(dest ...any) error {
|
|
*(dest[0].(*string)) = "publication-1"
|
|
*(dest[1].(*string)) = "kb-1"
|
|
*(dest[2].(*string)) = "knowledge-1"
|
|
return nil
|
|
}
|
|
|
|
type legacyVerificationRow struct {
|
|
id string
|
|
kbID string
|
|
knowledgeID string
|
|
}
|
|
|
|
func (row legacyVerificationRow) Scan(dest ...any) error {
|
|
*(dest[0].(*string)) = row.id
|
|
*(dest[1].(*string)) = row.kbID
|
|
*(dest[2].(*string)) = row.knowledgeID
|
|
return nil
|
|
}
|
|
|
|
type legacyVerificationRows struct {
|
|
items []legacyVerificationRow
|
|
index int
|
|
}
|
|
|
|
func (rows *legacyVerificationRows) Close() {}
|
|
func (rows *legacyVerificationRows) Err() error { return nil }
|
|
func (rows *legacyVerificationRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
|
|
func (rows *legacyVerificationRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
|
|
func (rows *legacyVerificationRows) Next() bool {
|
|
if rows.index >= len(rows.items) {
|
|
return false
|
|
}
|
|
rows.index++
|
|
return true
|
|
}
|
|
func (rows *legacyVerificationRows) Scan(dest ...any) error {
|
|
return rows.items[rows.index-1].Scan(dest...)
|
|
}
|
|
func (rows *legacyVerificationRows) Values() ([]any, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
func (rows *legacyVerificationRows) RawValues() [][]byte { return nil }
|
|
func (rows *legacyVerificationRows) Conn() *pgx.Conn { return nil }
|
|
|
|
type testPublicationRow struct {
|
|
id string
|
|
title string
|
|
ingest string
|
|
processing string
|
|
retrieval string
|
|
targetScope string
|
|
changeSetID string
|
|
changeType string
|
|
canStart bool
|
|
retryCount int
|
|
lastError string
|
|
canRetry bool
|
|
updated time.Time
|
|
}
|
|
|
|
func (row testPublicationRow) Scan(dest ...any) error {
|
|
*(dest[0].(*string)) = row.id
|
|
*(dest[1].(*string)) = row.title
|
|
*(dest[2].(*string)) = row.ingest
|
|
*(dest[3].(*string)) = row.processing
|
|
*(dest[4].(*string)) = row.retrieval
|
|
*(dest[5].(*string)) = row.targetScope
|
|
*(dest[6].(*string)) = row.changeSetID
|
|
*(dest[7].(*string)) = row.changeType
|
|
*(dest[8].(*bool)) = row.canStart
|
|
*(dest[9].(*int)) = row.retryCount
|
|
*(dest[10].(*string)) = row.lastError
|
|
*(dest[11].(*bool)) = row.canRetry
|
|
*(dest[12].(*time.Time)) = row.updated
|
|
return nil
|
|
}
|
|
|
|
type testPublicationRows struct {
|
|
items []testPublicationRow
|
|
index int
|
|
}
|
|
|
|
func (rows *testPublicationRows) Close() {}
|
|
func (rows *testPublicationRows) Err() error { return nil }
|
|
func (rows *testPublicationRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
|
|
func (rows *testPublicationRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
|
|
func (rows *testPublicationRows) Next() bool {
|
|
if rows.index >= len(rows.items) {
|
|
return false
|
|
}
|
|
rows.index++
|
|
return true
|
|
}
|
|
func (rows *testPublicationRows) Scan(dest ...any) error {
|
|
return rows.items[rows.index-1].Scan(dest...)
|
|
}
|
|
func (rows *testPublicationRows) Values() ([]any, error) { return nil, errors.New("not implemented") }
|
|
func (rows *testPublicationRows) RawValues() [][]byte { return nil }
|
|
func (rows *testPublicationRows) Conn() *pgx.Conn { return nil }
|
|
|
|
type totalRow struct {
|
|
total int
|
|
}
|
|
|
|
func (row totalRow) Scan(dest ...any) error {
|
|
*(dest[0].(*int)) = row.total
|
|
return nil
|
|
}
|
|
|
|
type acceptanceRecordRow struct {
|
|
checkType string
|
|
state string
|
|
policy string
|
|
evidence json.RawMessage
|
|
observed time.Time
|
|
}
|
|
|
|
func (row acceptanceRecordRow) Scan(dest ...any) error {
|
|
*(dest[0].(*string)) = row.checkType
|
|
*(dest[1].(*string)) = row.state
|
|
*(dest[2].(*string)) = row.policy
|
|
*(dest[3].(*json.RawMessage)) = row.evidence
|
|
*(dest[4].(*time.Time)) = row.observed
|
|
return nil
|
|
}
|
|
|
|
type acceptanceRecordRows struct {
|
|
items []acceptanceRecordRow
|
|
index int
|
|
}
|
|
|
|
func (rows *acceptanceRecordRows) Close() {}
|
|
func (rows *acceptanceRecordRows) Err() error { return nil }
|
|
func (rows *acceptanceRecordRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
|
|
func (rows *acceptanceRecordRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
|
|
func (rows *acceptanceRecordRows) Next() bool {
|
|
if rows.index >= len(rows.items) {
|
|
return false
|
|
}
|
|
rows.index++
|
|
return true
|
|
}
|
|
func (rows *acceptanceRecordRows) Scan(dest ...any) error {
|
|
return rows.items[rows.index-1].Scan(dest...)
|
|
}
|
|
func (rows *acceptanceRecordRows) Values() ([]any, error) { return nil, errors.New("not implemented") }
|
|
func (rows *acceptanceRecordRows) RawValues() [][]byte { return nil }
|
|
func (rows *acceptanceRecordRows) Conn() *pgx.Conn { return nil }
|
|
|
|
type retryStateRow struct {
|
|
processing string
|
|
ingest string
|
|
spoolAvailable bool
|
|
}
|
|
|
|
func (row retryStateRow) Scan(dest ...any) error {
|
|
*(dest[0].(*string)) = row.processing
|
|
*(dest[1].(*string)) = row.ingest
|
|
*(dest[2].(*bool)) = row.spoolAvailable
|
|
return nil
|
|
}
|
|
|
|
type publicationPollRow struct {
|
|
now time.Time
|
|
}
|
|
|
|
func (row publicationPollRow) Scan(dest ...any) error {
|
|
*(dest[0].(*string)) = "publication-1"
|
|
*(dest[1].(*string)) = "kb-1"
|
|
*(dest[2].(*string)) = "knowledge-1"
|
|
*(dest[3].(*string)) = string(governance.ProcessingProcessing)
|
|
*(dest[4].(*int)) = 2
|
|
*(dest[5].(*string)) = "processing"
|
|
*(dest[6].(**time.Time)) = &row.now
|
|
*(dest[7].(**time.Time)) = &row.now
|
|
*(dest[8].(**time.Time)) = &row.now
|
|
*(dest[9].(**time.Time)) = &row.now
|
|
return nil
|
|
}
|
|
|
|
type sequenceRow struct {
|
|
sequence int
|
|
}
|
|
|
|
func (row sequenceRow) Scan(dest ...any) error {
|
|
*(dest[0].(*int)) = row.sequence
|
|
return nil
|
|
}
|
|
|
|
func (publicationUploadRow) Scan(dest ...any) error {
|
|
values := []any{
|
|
"publication-1",
|
|
"version-1",
|
|
"kb-1",
|
|
"",
|
|
string(governance.IngestPending),
|
|
string(governance.ProcessingPending),
|
|
"internal",
|
|
"content-1",
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
int64(24),
|
|
"text/markdown",
|
|
"md",
|
|
"objects/artifact.md",
|
|
"Safe title",
|
|
}
|
|
for index, value := range values {
|
|
switch target := dest[index].(type) {
|
|
case *string:
|
|
*target = value.(string)
|
|
case *int64:
|
|
*target = value.(int64)
|
|
case *governance.Visibility:
|
|
*target = governance.Visibility(value.(string))
|
|
case nil:
|
|
panic("nil destination")
|
|
default:
|
|
panic("unexpected scan destination")
|
|
}
|
|
}
|
|
return nil
|
|
}
|