Skip to content

feat: [TKC-6336] mongo postgres convert - #8171

Open
vsukhin wants to merge 23 commits into
mainfrom
vsukhin/feature/mongo-postgres-convert
Open

feat: [TKC-6336] mongo postgres convert#8171
vsukhin wants to merge 23 commits into
mainfrom
vsukhin/feature/mongo-postgres-convert

Conversation

@vsukhin

@vsukhin vsukhin commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Pull request description

Checklist (choose whats happened)

  • breaking change! (describe)
  • tested locally
  • tested on cluster
  • added new dependencies
  • updated the docs
  • added a test

Breaking changes

Changes

Fixes

@vsukhin vsukhin changed the title Vsukhin/feature/mongo postgres convert feat: mongo postgres convert Aug 26, 2026
@vsukhin vsukhin changed the title feat: mongo postgres convert feat: [TKC-6336] mongo postgres convert Aug 26, 2026
Postgres has been a supported control-plane store for a while, selected by
setting API_POSTGRES_DSN instead of API_MONGO_DSN, but there was no way to
bring existing data across. Switching the DSN silently started from an empty
database: every past Test Workflow execution disappeared from the UI, and
execution numbering restarted at 1, so newly scheduled executions collided
with the names of old ones.

Add a convert binary that copies the two collections that hold data worth
moving - testworkflowresults into the seven test_workflow_* tables, and
sequences into execution_sequences. It runs as a Kubernetes Job during a
cutover, or directly against port-forwarded databases.

Nothing else is in scope, because nothing else is in Mongo. Execution logs,
outputs and artifacts live in object storage and only their references travel
with the execution. Workflow definitions and triggers are custom resources,
and cluster configuration is a ConfigMap. The triggers collection holds a
leader lease with a one-minute lifetime that the API reacquires on startup, so
copying it would move a stale row and nothing else.

The write path is CSV plus COPY FROM STDIN, mirroring the control plane's own
convert tool, including the per-execution all-or-nothing staging that stops a
serialization failure from importing a parent row without its children, and
the client-minted signature UUIDs that let a recursive tree satisfy its
self-referencing foreign key in file order.

Runs are resumable, which the tool this is modelled on is explicitly not. Each
batch commits its seven COPY statements and its checkpoint in one transaction,
so progress can never be durable without its data or vice versa. A restarted
Job resumes from the last committed batch instead of starting over or
double-writing - the difference between a tool an operator can retry and one
that has to be babysat.

Sequences upsert through a temporary table with GREATEST rather than a bare
COPY, so a counter can never move backwards. Lowering one would hand a
migrated execution's number to a new run, which is the failure the whole
sequences phase exists to prevent.

workflow_name and status are written directly rather than left to the two
denormalization triggers. Those still fire per row during COPY, but their
IS DISTINCT FROM guards then match nothing, so each costs an index probe
instead of a heap update plus index churn.

Documents are decoded raw rather than through the repository, whose read path
truncates long config values and blanks sensitive ones - storing that
projection would make the loss permanent. Dot-escaped map keys pass through
untouched, since both backends store them escaped.

Executions are validated before serializing: an empty string escapes to the
COPY null sentinel, so an execution missing a name would otherwise abort a
whole batch rather than one row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vsukhin
vsukhin force-pushed the vsukhin/feature/mongo-postgres-convert branch from 2dbd107 to f85e805 Compare August 26, 2026 14:20
@vsukhin

vsukhin commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a resumable MongoDB-to-PostgreSQL conversion tool, container build, Helm Job, checkpoint schema, verification, and migration tests. The previously reported trailing-failure checkpoint issue is addressed: checkpoint-only final commits advance past invalid documents, and reruns do not process them again.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
pkg/convert/executions.go Migrates execution documents in resumable batches and now persists checkpoint-only progress after trailing conversion failures.
pkg/convert/checkpoint.go Implements transactional loading, saving, completion, and clearing of conversion checkpoints.
pkg/convert/convert.go Coordinates migration tasks, reset behavior, verification, statistics, and completion reporting.
cmd/convert/main.go Adds the conversion CLI, database initialization, configuration validation, signal handling, and result reporting.
k8s/helm/testkube/templates/convert-job.yaml Adds an opt-in Kubernetes Job for running the one-shot database conversion.

Sequence Diagram

sequenceDiagram
    participant Job as Convert Job
    participant Mongo as MongoDB
    participant Converter as Converter
    participant Postgres as PostgreSQL
    Job->>Converter: Start conversion
    Converter->>Postgres: Load checkpoint
    Converter->>Mongo: Read documents after last_mongo_id
    loop Each batch
        Converter->>Converter: Map valid documents and record failures
        Converter->>Postgres: Begin transaction
        Converter->>Postgres: Copy mapped rows
        Converter->>Postgres: Save advanced checkpoint
        Converter->>Postgres: Commit transaction
    end
    Converter->>Postgres: Mark task complete
    Converter->>Postgres: Verify migrated counts
    Converter-->>Job: Report result
Loading

Reviews (4): Last reviewed commit: "fix: values" | Re-trigger Greptile

Comment thread pkg/convert/executions.go
flush() committed only when the batch held staged rows, but batchEnd also
advances for a document --skip-errors declined to migrate. So a run of
invalid documents that ended a batch - either falling after the last batch
that carried rows, or making up the whole run - moved the position without
anything committing it, and the task was marked complete with an older
checkpoint.

The next run then resumed before those documents, read them again, failed on
them again, and reported the same failures. Since a failed record makes the
run exit non-zero, a Job with a backoffLimit retried to no purpose and never
converged.

Gate the flush on whether the position has moved rather than on whether rows
were staged. A batch with nothing to copy still commits its checkpoint, so
declined documents are recorded once and stepped over from then on, which is
what --skip-errors asks for.

A commit that carries no rows is no longer counted as a batch, or the
statistics would report work that did not happen.

The completed flag is still not consulted when resuming, and now says why:
the stored position already makes a finished run a no-op, whereas honouring
the flag would refuse executions that arrived afterwards - which is exactly
the case when an operator converts ahead of the final cutover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vsukhin

vsukhin commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an OSS-facing MongoDB → PostgreSQL conversion tool and deployment plumbing to migrate historical TestWorkflow executions and execution counters during DB cutover, with resumability via Postgres checkpoints.

Changes:

  • Introduces cmd/convert and pkg/convert migrators (executions + sequences) with checkpointing and verification.
  • Adds convert_checkpoints Postgres migration to persist per-task progress and make runs resumable/idempotent.
  • Wires build/release + Helm support: Makefile target, Docker build target/image, GitHub workflow tags, and a Helm Job template/values.

Reviewed changes

Copilot reviewed 22 out of 24 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pkg/database/postgres/sqlc/models.go Adds a ConvertCheckpoint type in a sqlc-generated models file.
pkg/database/postgres/migrations/20260826120000_convert_checkpoints.sql Creates convert_checkpoints table used by the converter for crash-safe progress tracking.
pkg/convert/stats.go Per-task stats aggregation and printing.
pkg/convert/sequences.go Migrates Mongo sequences counters into execution_sequences with idempotent upsert semantics.
pkg/convert/sequences_test.go Unit tests for sequence name resolution and fold behavior.
pkg/convert/executiontime.go Repairs legacy execution timestamps/durations to avoid epoch/overflow artifacts.
pkg/convert/executions.go Batch migrator for executions with exactly-once checkpoint commit semantics.
pkg/convert/executions_row.go COPY-row serializers and column order definitions for the target tables.
pkg/convert/executions_row_test.go Unit tests for COPY serialization correctness and escaping.
pkg/convert/copyutil.go COPY helpers: escaping, JSON sanitization, timestamp formatting, and COPY streaming.
pkg/convert/copyutil_test.go Unit tests for escaping/sanitization/timestamp formatting.
pkg/convert/convert.go Orchestrates tasks, reset, verify, and run summary reporting.
pkg/convert/convert_integration_test.go Integration tests asserting end-to-end migration correctness and idempotency/resume behavior.
pkg/convert/checkpoint.go Checkpoint load/save/complete/clear helpers against convert_checkpoints.
pkg/convert/checkpoint_test.go Unit test for batch counting semantics.
pkg/controlplane/scheduling/sqlc/models.go Adds a ConvertCheckpoint type in a sqlc-generated models file.
Makefile Adds build-convert and includes it in make build.
k8s/helm/testkube/values.yaml Adds convert: values block for enabling/configuring the conversion Job.
k8s/helm/testkube/templates/convert-job.yaml New Helm Job template to run the convert tool during cutover.
k8s/helm/testkube/templates/_helpers.tpl Adds helper functions for convert Job naming/labels/image/env wiring.
docker-bake.hcl Adds a convert bake target using build/new/convert.Dockerfile.
cmd/convert/main.go CLI entrypoint for the conversion tool (flags/envs, DB connect, migrations, execution).
build/new/convert.Dockerfile Builds and packages the convert binary into an Alpine-based runtime image.
.github/workflows/new-build.yaml Adds build+publish metadata/targets for the convert image in CI.
Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/controlplane/scheduling/sqlc/models.go
Comment thread Makefile Outdated
Comment thread cmd/convert/main.go Outdated
Comment thread cmd/convert/main.go Outdated
Comment thread pkg/database/postgres/sqlc/models.go
vsukhin and others added 5 commits August 26, 2026 18:31
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@vsukhin
vsukhin requested a lite review from Copilot August 26, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 24 changed files in this pull request and generated 8 comments.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file

Comment thread pkg/convert/sequences.go
Comment thread pkg/convert/sequences.go Outdated
Comment thread pkg/convert/executions.go
Comment thread pkg/convert/executions.go Outdated
Comment thread pkg/convert/executions.go Outdated
Comment thread pkg/convert/executions.go Outdated
Comment thread cmd/convert/main.go Outdated
Comment thread cmd/convert/main.go
vsukhin and others added 4 commits August 26, 2026 16:15
Both migrators deferred stats.finish() and then called it again before
printing, so the lifecycle read as though Print needed a settled EndTime to
report a duration. It does not: Duration falls back to the time elapsed so
far precisely so a caller can defer its single finish call, and the deferred
call already covers every return path.

Drop the explicit calls and say so on Duration, so the fallback is not
mistaken for an oversight and the second call reintroduced.

Also collapse the blank line left where ConvertCheckpoint was removed from
the two generated model files, which left them failing gofmt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ConvertCheckpoint had been removed from both generated model files by hand,
but the goose migration still creates convert_checkpoints and sqlc emits a
model for every table in the schema, so the removal did not survive
generation - the next `make generate` would have put it back, and until then
the generated files disagreed with their generator.

Regenerate so they match again. The struct is unused, since the convert tool
reaches convert_checkpoints through hand-written SQL rather than sqlc, but a
generated file is not the place to prune dead code: keeping it out means
either dropping the migration and creating the table from the tool, or
teaching sqlc to exclude it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two leaks on the way out, both of which only show up on the path a Job
actually takes when it is told to stop.

The *sql.DB wrapping the pool for goose was never closed. It borrows
connections from the pool and runs an opener goroutine, and pgx documents
that closing it leaves the pool alone - which is what makes closing it safe
here, since the pool is what the function returns. The repository's own
Postgres test helper already closes its wrapper; this now matches.

The deferred Mongo disconnect ran on the main context, which by then has
been cancelled by the SIGINT/SIGTERM handler set up a few lines above. A
cancelled context makes Disconnect give up before closing anything, so the
one case the defer exists for - shutting down on a signal - was the case it
did not cover. Cleanup gets a fresh context with its own deadline, bounded
so a hung close cannot hold the pod open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every iteration unmarshalled the document once into a one-field struct to
get _id and again into the execution. The first decode still walks the whole
document - resolvedWorkflow and every step result included - to reach a
field sitting at its head.

Look _id up in the raw bson instead. MongoDB writes it first, so the lookup
stops almost immediately, and it neither allocates nor leaves bson. Measured
on a document carrying two workflow snapshots and 200 step results: 50ns and
no allocations, against 1421ns and 12 allocations for the decode it replaces.

The lookup also lets a bad _id be reported precisely rather than as a decode
failure, and it stays fatal whatever --skip-errors says: the checkpoint
stores the position as a hex ObjectID, so an _id it cannot represent leaves
no resume point, and continuing would checkpoint the wrong place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file

Comment thread pkg/convert/executions_row.go Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file

Comment thread build/new/convert.Dockerfile

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated no new comments.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file
Suppressed comments (4)

Previously missed (2) — in code that hasn't changed since the last review.

pkg/convert/convert.go:238

  • Verification uses per-run Failed/Skipped counts to compute the expected Postgres row count. On a resumed run, prior failures/skips are stored only in the checkpoint, so this can produce a false warning (and mark the run failed) even though there is nothing left to migrate.

This issue also appears on line 258 of the same file.

		expected := stats.Total - stats.Failed - stats.Skipped
		c.log.Infof("Executions: %d in mongo, %d in postgres (%d expected)", stats.Total, pgCount, expected)
		if pgCount < expected {
			warnings = append(warnings, fmt.Sprintf(
				"executions: postgres holds %d rows but %d were expected (%d in mongo, %d failed, %d skipped)",

pkg/convert/stats.go:58

  • Stats keeps every individual error string in memory. With --skip-errors on a large/dirty dataset this can grow without bound and potentially OOM the Job even though only a capped subset is ever reported.

This issue also appears on line 113 of the same file.

func (s *Stats) addError(format string, args ...interface{}) {
	s.Failed++
	s.Errors = append(s.Errors, fmt.Sprintf(format, args...))
}

pkg/convert/convert.go:262

  • Same as executions: verification derives the expected count from per-run counters. If the task is re-run after a previous run recorded failures/skips in the checkpoint, verification can warn incorrectly.
		expected := stats.Total - stats.Failed - stats.Skipped
		c.log.Infof("Sequences: %d in mongo, %d in postgres (%d migratable)", stats.Total, pgCount, expected)
		if pgCount < expected {
			warnings = append(warnings, fmt.Sprintf(
				"sequences: postgres holds %d rows but %d were expected (%d in mongo, %d skipped as non-testworkflow)",

pkg/convert/stats.go:124

  • After capping stored error messages, Print should report total error count using the Failed counter (not len(Errors)) and still show how many additional errors were omitted.
	if len(s.Errors) == 0 {
		return
	}

	log.Warnf("Encountered %d errors (showing up to %d):", len(s.Errors), maxReportedErrors)
	for i, e := range s.Errors {
		if i >= maxReportedErrors {
			log.Warnf("... and %d more", len(s.Errors)-maxReportedErrors)
			break
		}
		log.Warnf("  %d. %s", i+1, e)
	}

@vsukhin

vsukhin commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

Verification compares the whole source collection against the whole target
table, but derived the expected row count from the counters of the run that
had just finished. Those describe only the documents that run read. A resumed
run with nothing left to do reported zero failures, so every document an
earlier run had declined looked like a missing row: a warning, a non-zero
exit, and a Job that retried a migration which was already complete.

Report the totals across all runs alongside the per-run ones and verify
against those. The two tasks need different totals, so each says which it
means rather than sharing a rule that happens to suit one of them: executions
resume from a position and add the checkpoint's totals to their own, while
sequences reread the whole collection every time and must not, or the same
skipped documents would be counted once per run and drive the expected count
negative.

Result.Failed still keys off the per-run counters, so a run that had nothing
to do and nothing go wrong exits zero even when earlier runs recorded
failures. There is nothing left for a retry to achieve.

Retained error messages are now capped. A damaged collection run with
--skip-errors kept one string per failed document in a process that is
already holding a batch. Failed stays exact and Print reports it rather than
the number of messages kept, so the summary cannot understate the damage,
and the omitted tail is counted against the true total.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/convert/copyutil.go:56

  • escapeJSONB uses string(data) == "null", which allocates a new string for every JSONB field serialized. This is on the hot path for execution migration (many JSONB columns per row). Prefer a byte comparison to avoid per-call allocations.
func escapeJSONB(data []byte) string {
	if len(data) == 0 || string(data) == "null" {
		return copyNull
	}
	return escapeCopySpecials(string(sanitizeJSONForPG(data)))

Comment thread pkg/convert/sequences.go
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file

Comment thread pkg/convert/sequences.go
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/convert/executions.go:439

  • In executions checkpointing, cp.Completed is never cleared during a re-run. If a previous run set completed_at, then a later run that migrates new documents (or is interrupted mid-run) will keep writing completed_at as non-NULL in saveCheckpoint, making the checkpoint look "completed" even while work is in progress or incomplete.
	cp.Processed = m.priorProcessed + stats.Processed + counts.executions
	cp.Failed = m.priorFailed + stats.Failed
	cp.Skipped = m.priorSkipped + stats.Skipped
	if err := saveCheckpoint(ctx, tx, cp); err != nil {
		return err

Comment thread pkg/convert/convert.go Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated no new comments.

Files not reviewed (2)
  • pkg/controlplane/scheduling/sqlc/models.go: Generated file
  • pkg/database/postgres/sqlc/models.go: Generated file
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

pkg/convert/sequences.go:199

  • When there are zero migratable rows, write() returns early without persisting the checkpoint. That means a collection containing only non-testworkflow/legacy sequences will be re-scanned and re-warned on every run, and the task will never be marked completed in convert_checkpoints.
	if len(rows) == 0 {
		m.log.Info("No test workflow sequences to migrate")
		return nil
	}

Signed-off-by: Vladislav Sukhin <vladislav@kubeshop.io>
Signed-off-by: Vladislav Sukhin <vladislav@kubeshop.io>
@vsukhin
vsukhin marked this pull request as ready for review August 27, 2026 14:24
@vsukhin
vsukhin requested review from a team as code owners August 27, 2026 14:24
@vsukhin
vsukhin requested review from buarki and ypoplavs August 27, 2026 14:24
vsukhin and others added 4 commits August 27, 2026 15:13
The integration fixture built a workflow snapshot with no Spec, and
TestWorkflow.ConvertDots guards a nil workflow but then reaches through its
Spec unguarded. So the Mongo repository's Insert, which escapes dots before
writing, segfaulted while the fixture was still being seeded and took the
whole package's tests down with it.

Real snapshots always carry a spec, so the fixture was describing data no
installation holds. Give it one, and put a dotted label inside it while we
are here: the spec travels as a single JSONB column, so that also covers
escaping surviving a round trip through it.

Two tests now pin this without needing a database. One asserts the fixture
survives the escaping and unescaping the repositories apply, which is the
call that panicked; the other asserts the escaping round-trips and that the
fixture really does contain dotted keys, so it cannot quietly stop testing
anything. Waiting for the integration job to catch a fixture that does not
match the model was the actual mistake.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two differences the cross-backend comparison was reading as migration bugs.

Timestamps came back in different locations: BSON carries no zone, so the
Mongo driver decodes in the local one, while pgx returns TIMESTAMPTZ in UTC.
Both name the same instant, but reflect-based equality compares the Location
pointer too, so the comparison could only have passed on a machine whose local
zone is UTC - and there for the wrong reason. Compare with go-cmp and a
comparer that asks whether two times are equal, which also names the differing
field instead of printing both executions in full.

The fixture also carried an empty resource aggregations report, whose two
JSONB columns are both NULL. PostgresRepository.Get rebuilds the report only
when one of them holds something, so it came back nil there and intact from
Mongo. PostgresRepository.Insert stores an empty report exactly as the
migrator does, so this is the repositories disagreeing about an absent child,
not the migration losing one. Give the fixture real aggregations.

Reports has the same shape of disagreement in the other direction:
MongoRepository.Insert defaults a nil Reports to an empty slice, so Mongo
serves [] where Postgres serves nil. Nothing trips it today because the
fixture populates reports, so one test now pins the rule for every child
collection and records why, rather than leaving the next minimal execution
added to that comparison to rediscover it.

Both fixes are pinned by tests that need no database: one that the comparer
ignores location but not instant, one that the fixture populates every child.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
markCheckpointComplete wrote unconditionally, so a dry run left a checkpoint
row behind claiming the task had finished. A later real run would then resume
from a position nothing had been migrated to, and skip everything up to it.

Every other writer on this path already declines a dry run itself; this one
being the exception is what let the write through. Give it the flag too,
rather than adding a fourth caller-side check to a guard that has now been
missed once.

Audited the rest while here. The two truncates and the checkpoint clear are
reachable only through reset, which returns early on a dry run, and all three
saveCheckpoint calls sit inside guarded functions, so the data path is now
covered. Schema is the deliberate exception and both the field and the flag
now say so: the checkpoint lookup and the verification counts read tables
that have to exist first, so a dry run still creates the database and applies
migrations.

The dry-run test now asserts every table the migration touches is empty,
convert_checkpoints included. Checking only the tables a previous leak
touched is how the next leak gets missed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@testkubebot

testkubebot Bot commented Aug 28, 2026

Copy link
Copy Markdown

✅ Testkube GitHub Integration

Review based on commit bab7853.

All tests and quality gates passed.


Phase Status
Test Workflow Execution ✅ Passed
Quality Gate ✅ Passed

7 workflows executed

lint-go passed
in 4m17s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:15:40 UTC)

lint-proto passed
in 9s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:11:32 UTC)

integration-tests passed
in 8m10s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:19:33 UTC)

unit-tests passed
in 4m22s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:15:45 UTC)

verify-crds passed
in 1m20s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:12:43 UTC)

verify-protobuf passed
in 19s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:11:41 UTC)

lint-pr passed
in 10s (🚀 28. Aug. 2026 - 14:11:22 UTC / 🏁 28. Aug. 2026 - 14:11:32 UTC)


Manage this Integration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants