Skip to content

fix(output-mapping): preserve production table typing in branches [AJDA-3014]#540

Open
MiroCillik wants to merge 3 commits into
mainfrom
devin/1784025586-ajda-3014-preserve-branch-table-typing
Open

fix(output-mapping): preserve production table typing in branches [AJDA-3014]#540
MiroCillik wants to merge 3 commits into
mainfrom
devin/1784025586-ajda-3014-preserve-branch-table-typing

Conversation

@MiroCillik

@MiroCillik MiroCillik commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

Fixes AJDA-3014 (SUPPORT-16987): Branched Storage was creating non-typed production tables as typed on their first write inside a development branch, because automatic typed-table creation was applied to what is effectively a pre-existing table just being materialized into the branch for the first time. After merge to production the transformation (built/tested against the typed branch table) then breaks against the still non-typed production table.

The fix consults the corresponding default-branch table when a destination is created for the first time in Branched Storage, and only allows automatic typed-table creation when the production table doesn't exist yet or is already typed:

// StoragePreparer::prepareStorageBucketAndTable
$defaultBranchTableInfo = null;
if ($destinationTableInfo === null
    && $clientOptions->useBranchStorage()
    && $clientWrapper->isDevelopmentBranch()
) {
    $defaultBranchTableInfo = $this->getDefaultBranchTableInfoIfExists($tableId); // getClientForDefaultBranch()->getTable(), 404 => null
}
return new MappingStorageSources($destinationBucket, $destinationTableInfo, $defaultBranchTableInfo);

// LoadTableTaskCreator
$defaultBranchTable = $storageSources->getDefaultBranchTable();
$canCreateTypedTable = $defaultBranchTable === null || $defaultBranchTable->isTyped();
// $canCreateTypedTable now gates both the hasNativeTypesFeature and hasNewNativeTypesFeature typed-creation branches

A production non-typed table therefore stays non-typed in the dev branch even when the output has an authoritative schema. New branch-only tables and existing branch tables keep their current behavior.

When typed creation is suppressed for a schema-based source, LoadTableTaskCreator now creates a plain non-typed table using the schema's column names (the DDL-first createTable path) instead of falling through to the CreateAndLoadTableTask "unknown columns" branch. Without this, a headless CSV (as produced by native-types transformations) would be imported with its first data row taken as the header, leaving the branch table with garbage column names. This branch is gated on !$canCreateTypedTable, so no previously-passing scenario changes behavior.

This ports the reviewed fix from the read-only standalone repo (keboola/output-mapping#1) into the monorepo source at libs/output-mapping, since that repo is a read-only publish target.

Test coverage

  • LoadTableTaskCreatorTest — unit-level tests locking the $canCreateTypedTable gate: typed creation is suppressed when the default-branch table is non-typed (asserting the resulting table is non-typed with the correct columns), and still allowed when the default-branch table is typed.
  • StorageApiLocalTableWriterTesttestFirstDevBranchWritePreservesNonTypedProductionTable now asserts columns and row data (not just the isTyped flag), plus two positive end-to-end cases proving the fix does not over-suppress: branch tables are still created typed when the production table is typed or absent (the latter also exercising the 404 path of getDefaultBranchTableInfoIfExists).

Scope & known limitations

This is deliberately a step-1 "stop the bleeding" stopgap, not the full fix:

  • One-directional mirror. It only gates off typed creation; it never forces it. The reported break is strictly one-directional (non-typed prod → typed branch → error after merge), and that direction is fully covered. The inverse case (prod typed, but the transformation itself would not produce a typed table) still yields a non-typed branch table — a harmless divergence that belongs to the follow-up convergence work, not this stopgap.
  • Interim layer. The "branch mirrors prod on first materialization" invariant is implemented here client-side in output-mapping. Structurally it belongs in Branched Storage (Connection/Storage side), where lazy branch-table creation actually happens. This change is localized and trivially removable once the invariant moves down, so it introduces no architectural lock-in.

Follow-ups tracked separately:

  • AJDA-3053 — merge-aware "switch table to native types" action (declared retype property that survives merge).
  • AJDA-3054 — typing as a declared property enforced by output mapping (recreate-if-typing-differs) + the branch-mirrors-prod invariant living in Branched Storage.

Release Notes

  • Justification

    • Branched Storage was silently upgrading pre-existing non-typed production tables to typed tables the first time they were materialized into a development branch. Transformations developed and tested against the typed branch table (without the NULLIF/cast guards that a non-typed table needs) then failed once merged to production, where the table is still non-typed. This breaks the core promise of development branches — that what you test in a branch behaves the same after merge.
    • The change makes a branch preserve the production table's current typing status on first write: a non-typed production table stays non-typed in the branch (with its correct columns), regardless of the transformation's automatic data-type detection setting. Automatic detection continues to apply only to genuinely new tables.
  • Plans for Customer Communication

    • No proactive customer communication required for the stopgap; it restores the expected/documented behavior and removes a data-integrity footgun. The reporting customer (Košík, SUPPORT-16987) should be notified that the branch-vs-production typing mismatch is fixed. Larger follow-up work (explicit native-types migration action) will be communicated separately when delivered.
  • Impact Analysis

    • Affects only output mapping when writing into Branched Storage in a development branch, on the first write of a table that does not yet exist in the branch. It adds one extra Storage API getTable call against the default branch in that narrow path. No impact on production writes, non-branch writes, or writes to tables already present in the branch. No single-tenant-specific impact.
    • Not behind a feature flag; behavior change is scoped to the branch-storage first-write path described above.
  • Deployment Plan

    • Library release via the standard monorepo split/publish; consumed by output-mapping consumers (job runner) on their next dependency bump. Continuous deployment, no stack-by-stack rollout required. Not critical.
  • Rollback Plan

    • Two-way door. The change is localized to output-mapping and can be reverted/redeployed independently; reverting restores the previous (buggy) behavior with no data migration needed.
  • Post-Release Support Plan

    • No special ongoing support needed. Support team can close SUPPORT-16987 referencing this fix once released. Follow-up issues (AJDA-3053, AJDA-3054) track the fuller native-types migration story.

Link to Devin session: https://app.devin.ai/sessions/2cf3e8a34494418daea07c967b0cd25e
Requested by: @MiroCillik

…DA-3014]

Co-Authored-By: Miro Čillík <mcillik@gmail.com>
@MiroCillik MiroCillik self-assigned this Jul 14, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@linear-code

linear-code Bot commented Jul 14, 2026

Copy link
Copy Markdown

AJDA-3014

@MiroCillik
MiroCillik marked this pull request as ready for review July 23, 2026 09:08

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: needs_human (risk 3/5) · profile ajda

Behavior change in published platform-libraries code with no feature flag — load-bearing per policy, so a human should review.

Concerns:

  • libs/output-mapping/src/LoadTableTaskCreator.php: Typed-table creation semantics changed in published library, no feature flag to disable
  • libs/output-mapping/src/Storage/StoragePreparer.php: New default-branch getTable call on branch first-write path; ungated observable behavior change
  • libs/output-mapping/tests/Writer/StorageApiLocalTableWriterTest.php: Bare ->method('hasFeature') without expects(); violates AJDA PHPUnit convention

Suggested reviewers: MiroCillik

MiroCillik and others added 2 commits July 23, 2026 11:31
Add regression coverage for the AJDA-3014 fix that preserves a production
table's typing status when it is first materialized into a development
branch:

- LoadTableTaskCreatorTest: unit-level tests locking the $canCreateTypedTable
  gate — typed creation is suppressed when the default-branch table is
  non-typed, and still allowed when it is typed.
- StorageApiLocalTableWriterTest: strengthen the non-typed regression test to
  assert columns and row data (not just the isTyped flag), and add positive
  end-to-end cases proving the fix does not over-suppress — branch tables are
  still created typed when the production table is typed or absent (the latter
  also exercising the 404 path of getDefaultBranchTableInfoIfExists).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssed [AJDA-3014]

The strengthened regression test revealed that when typed-table creation is
suppressed for a non-typed production table (AJDA-3014), a schema-based source
with a headless CSV fell through to the CreateAndLoadTableTask "unknown
columns" branch (schema-only => hasColumns() is false). That path creates and
loads in a single queueTableCreate call with no pre-created columns, so the
Storage API took the first data row as the header — the branch table ended up
with columns named after the data (e.g. "2", "development").

Add a dedicated branch: when typed creation is suppressed but the schema is
known, create a plain non-typed table with the schema's column names (the same
DDL-first path used for manifest columns), so a headless CSV is loaded
correctly. Gated on !$canCreateTypedTable so no currently-passing scenario
changes behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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