Skip to content

feat(storage): apply_deployments table + CRUD + tasks.apply_deployment_id (foundation)#205

Open
Kiran01bm wants to merge 3 commits into
mainfrom
kiran01bm/multi-deployment-apply-deployments-foundation
Open

feat(storage): apply_deployments table + CRUD + tasks.apply_deployment_id (foundation)#205
Kiran01bm wants to merge 3 commits into
mainfrom
kiran01bm/multi-deployment-apply-deployments-foundation

Conversation

@Kiran01bm
Copy link
Copy Markdown
Collaborator

@Kiran01bm Kiran01bm commented Jun 1, 2026

What

Storage foundation for the Multi-Deployment Apply model. Bundles MD-1 + MD-2 + MD-4 (WS-MD-F).

  • MD-1 — new apply_deployments table: one row per (apply, deployment), unique on (apply_id, deployment), states pending/in_progress/completed/failed, index (deployment, state).
  • MD-2storage.ApplyDeployment + state.ApplyDeployment + ApplyDeploymentStore interface (Insert/Get/List/UpdateState/MarkStarted/MarkCompleted/MarkFailed/DeleteByApply) + MySQL impl + 20 integration tests.
  • MD-4 — nullable tasks.apply_deployment_id + index. Not added to taskColumns yet; wired up in MD-6.
  • testutil.ColumnExists helper; TestEnsureSchema asserts new table + column.

Why

Per-deployment apply state needs its own row so the active-apply lock and scheduler can key on (deployment, state) instead of overloading applies. This PR lands the schema + CRUD only — pure additive, zero behaviour change, no callers touch the new artifacts yet.

Chosen design: 03-multi-deployment-apply-model.md. PR #194 re-scoped to stage A only; B–E superseded by WS-MD.

Bundled because MD-1 (table) is unusable without MD-2 (store), and MD-4 shares the same migration window; splitting would produce three PRs that all need to land before anything else proceeds.

Follow-ups

MD-3 (config), MD-5 (dual-write), MD-6 (scheduler + lock move), MD-7..11.

Validation

go build, go vet, unit tests ✅ (incl. -tags integration). TestApplyDeploymentStore integration tests not run locally (Docker Desktop / Testcontainers API mismatch); CI will exercise.

…t_id (foundation)

Lay down the storage foundation for the multi-deployment apply data
model (WS-MD-F = MD-1 + MD-2 + MD-4 bundled). Pure additive, zero
behaviour change, no caller reads or writes any of the new artifacts
yet — those land in MD-3 (config) and MD-5/MD-6 (write path, scheduler).

MD-1 — new `apply_deployments` table
  One child row per (apply, deployment). Unique on (apply_id, deployment).
  4-state machine: pending / in_progress / completed / failed. Acts as
  the per-deployment slice that MD-5 will dual-write into and MD-6 will
  move the active-apply lock onto.

MD-2 — Go model + ApplyDeploymentStore CRUD
  - storage.ApplyDeployment struct (pkg/storage/types.go).
  - state.ApplyDeployment + IsApplyDeploymentTerminal (pkg/state/).
  - ApplyDeploymentStore interface (pkg/storage/storage.go):
    Insert / Get / GetByApplyDeployment / ListByApply / UpdateState /
    MarkStarted / MarkCompleted / MarkFailed / DeleteByApply.
  - MySQL impl (pkg/storage/mysqlstore/apply_deployments.go); unique-key
    conflict on (apply_id, deployment) returns ErrApplyDeploymentExists.
  - Wired into mysqlstore.Storage; mockStorage stub added in the api
    package tests so the interface change stays compile-clean.
  - 20 integration tests (pkg/storage/mysqlstore/apply_deployments_test.go):
    insert/get/list, isolation across applies, unique constraint,
    state transitions (with started_at COALESCE), MarkFailed, DeleteByApply,
    not-found and DB-error paths.

MD-4 — tasks.apply_deployment_id nullable FK
  New nullable column + idx_apply_deployment_id on `tasks`. Intentionally
  not added to `taskColumns`, so no existing Task read/write path scans
  or writes it — MD-6 turns the write path on once the scheduler claims
  per apply_deployments row.

Test utility: pkg/testutil/container.go grows a ColumnExists helper
mirroring TableExists.

TestEnsureSchema asserts both `apply_deployments` table and
`tasks.apply_deployment_id` column land via EnsureSchema.

Background and full plan:
  WS-MD implementation plan (internal) lists this PR as MD-F and the
  remaining sequence (MD-3, MD-5..MD-11). Approach was chosen in the
  Slack thread on PR #194; the picked design is the
  "Multi-Deployment Apply (apply_deployments join table)" model — keeps
  apply_id as the user-facing handle, pushes per-deployment state +
  locking down into the new child rows.
Copilot AI review requested due to automatic review settings June 1, 2026 11:10
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR lays the storage/schema foundation for the Multi-Deployment Apply data model by introducing an apply_deployments child table plus a new storage interface/implementation for CRUD and state transitions, and by adding a nullable tasks.apply_deployment_id column for future wiring.

Changes:

  • Add MySQL DDL for new apply_deployments table and extend tasks with nullable apply_deployment_id + index.
  • Introduce storage.ApplyDeployment, state.ApplyDeployment constants, and ApplyDeploymentStore interface + MySQL implementation.
  • Add integration coverage for the new store and EnsureSchema assertions (including a new testutil.ColumnExists helper).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/testutil/container.go Adds ColumnExists helper for schema verification tests.
pkg/storage/types.go Introduces storage.ApplyDeployment struct type.
pkg/storage/storage.go Extends Storage with ApplyDeployments() and adds ApplyDeploymentStore interface.
pkg/storage/mysqlstore/storage.go Wires MySQL storage to provide an ApplyDeploymentStore.
pkg/storage/mysqlstore/apply_deployments.go Implements MySQL CRUD + state transition operations for apply_deployments.
pkg/storage/mysqlstore/apply_deployments_test.go Adds integration tests covering store behavior and error cases.
pkg/storage/errors.go Adds typed storage errors for apply-deployment not found/exists.
pkg/state/apply_deployment.go Adds apply-deployment state constants + terminal-state helper.
pkg/schema/schema.go Updates schema package table list comment to include apply_deployments.
pkg/schema/mysql/tasks.sql Adds nullable apply_deployment_id column + index to tasks.
pkg/schema/mysql/apply_deployments.sql Adds DDL for apply_deployments table and indexes.
pkg/api/handlers_test.go Updates mock storage for new interface method; uses slices.Backward for reverse scan.
pkg/api/ensure_schema_integration_test.go Ensures new table exists and asserts tasks.apply_deployment_id column is present.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/storage/storage.go Outdated
Comment thread pkg/storage/mysqlstore/apply_deployments.go Outdated
Comment thread pkg/storage/mysqlstore/apply_deployments.go Outdated
Comment thread pkg/storage/storage.go Outdated
Comment thread pkg/storage/mysqlstore/apply_deployments.go Outdated
Comment thread pkg/storage/mysqlstore/apply_deployments.go Outdated
Address Copilot review on PR #205.

- storage.ApplyDeploymentStore.UpdateState doc no longer implies it writes
  started_at/completed_at; directs callers to Mark* for timestamped transitions.
- mysqlstore.applyDeploymentStore.UpdateState and MarkStarted no longer
  surface ErrApplyDeploymentNotFound on idempotent retries. MySQL's default
  RowsAffected counts changed rows (not matched), so re-applying the same
  state, or re-issuing MarkStarted on an already-started row (COALESCE),
  reports 0 affected. New checkUpdatedOrExists helper disambiguates via
  SELECT EXISTS.
- Add regression tests covering both idempotent paths.
@Kiran01bm Kiran01bm marked this pull request as ready for review June 1, 2026 11:30
@Kiran01bm Kiran01bm requested review from aparajon and morgo as code owners June 1, 2026 11:30
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2223aa164a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/storage/mysqlstore/apply_deployments.go Outdated
Comment thread pkg/api/ensure_schema_integration_test.go Outdated
Comment thread pkg/state/apply_deployment.go Outdated
Comment on lines +8 to +11
// the lifecycle of a single deployment target (one Tern endpoint, one lock),
// so the engine-specific intermediate states (waiting_for_deploy,
// waiting_for_cutover, etc.) do not apply at this layer. Those still live on
// the apply / task it spawned.
Copy link
Copy Markdown
Collaborator

@aparajon aparajon Jun 1, 2026

Choose a reason for hiding this comment

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

I think each apply_deployment will need to have the same apply states 😅 . Some use-cases:

  • It should be possible to cutover one apply_deployment while others are still waiting, which is needed for operator controlled rollouts (e.g. test in one deployment before rolling out to others progressively)
  • Triaging: operators should be able to independent track each rollout, and have the ability to remediate in the case that the scheduler can't auto-retry

This means apply state will need to be derived from apply_deployment states, similar to how apply is derived from task state today

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

thats a good point - i'll ensure that the apply_deployment / apply_operation carries the full apply state machine

… state machine

Address PR #205 review feedback (Slack thread + inline comments):

- Rename table, struct, store, errors, FK column, indexes, tests, and
  filenames to apply_operations. Each row carries both a deployment and a
  target, so "apply operation" matches the unit-of-work semantics
  (companion rename scheduler → operator lands in MD-6 where the claim
  loop actually moves).
- Rename misleading GetByApplyOperation(applyID, deployment) helper to
  GetByApplyAndDeployment to match its parameter shape.
- Expand state.ApplyOperation to mirror state.Apply exactly — same 12
  canonical states plus the PlanetScale-specific lifecycle phases. Per-row
  cutover / stop / revert / triage now share the apply state vocabulary,
  and the upcoming applies.state derivation can reuse DeriveApplyState()
  verbatim instead of growing a parallel aggregator. MarkStarted now
  writes state=running (not in_progress).
- Fix MarkCompleted / MarkFailed same-second idempotency: route both
  through checkUpdatedOrExists and switch completed_at to
  COALESCE(completed_at, NOW()) so a retried no-op write on an already-
  terminal row no longer returns ErrApplyOperationNotFound. Adds
  TestApplyOperationStore_MarkCompleted_Idempotent and
  TestApplyOperationStore_MarkFailed_Idempotent regression tests.
- Drop internal MD-* workstream tracking labels from PR-visible comments
  (they're sequencing labels, not stable upstream vocabulary).

Verified: go build, go build -tags integration, go vet,
go vet -tags integration, and the pkg/state / pkg/storage / pkg/schema /
pkg/api unit suites all pass. Testcontainers integration tests for the
new store will run on CI (Docker Desktop API mismatch locally).
Comment thread pkg/storage/errors.go

// ErrApplyOperationNotFound is returned when an apply_operations child
// row does not exist for the given lookup key.
ErrApplyOperationNotFound = errors.New("apply deployment not found")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should be apply operation

Comment thread pkg/storage/errors.go

// ErrApplyOperationExists is returned when an apply_operations row for
// (apply_id, deployment) is being inserted but already exists.
ErrApplyOperationExists = errors.New("apply deployment already exists")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should be apply operation

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.

3 participants