Skip to content

fix(destination-motherduck): handle camel cased object/array columns - #83694

Merged
Aaron ("AJ") Steers (aaronsteers) merged 2 commits into
airbytehq:masterfrom
Donnype:donnypy/fix-motherduck-serialization-normalized-columns
Aug 4, 2026
Merged

fix(destination-motherduck): handle camel cased object/array columns#83694
Aaron ("AJ") Steers (aaronsteers) merged 2 commits into
airbytehq:masterfrom
Donnype:donnypy/fix-motherduck-serialization-normalized-columns

Conversation

@Donnype

Copy link
Copy Markdown
Contributor

What

The empty-struct bug and upper-cased columns bug joined forces and produced a new bug: upper-cased-empty-struct-columns are broken. After #82244 this became more apparent since now also arrays pass that logic. This PR applies the same normalisation logic to that edge-case handling.

How

Pass the normalizer and apply the normalization to the JSON keys (column names) before moving forward.

Review guide

It's mostly test code, the real logic is the dict-comprehension changing the keys.

User Impact

The sync is failing for more complicated APIs that happen to use camel/pascalCase. This is blocking users from using Airbyte for ingest.

Can this PR be safely reverted and rolled back?

  • YES 💚
  • NO ❌

@octavia-bot

octavia-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Note

📝 PR Converted to Draft

More info...

Thank you for creating this PR. As a policy to protect our engineers' time, Airbyte requires all PRs to be created first in draft status. Your PR has been automatically converted to draft status in respect for this policy.

As soon as your PR is ready for formal review, you can proceed to convert the PR to "ready for review" status by clicking the "Ready for review" button at the bottom of the PR page.

To skip draft status in future PRs, please include [ready] in your PR title or add the skip-draft-status label when creating your PR.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

👋 Welcome to Airbyte!

Thank you for your contribution from Donnype/airbyte! We're excited to have you in the Airbyte community.

If you have any questions, feel free to ask in the PR comments or join our Slack community.

💡 Show Tips and Tricks

PR Slash Commands

As needed or by request, Airbyte Maintainers can execute the following slash commands on your PR:

  • /format-fix - Fixes most formatting issues.
  • /bump-version - Bumps connector versions.
  • /run-connector-tests - Runs connector tests.
  • /run-cat-tests - Runs CAT tests.
  • /run-regression-tests - Runs regression tests for the modified connector(s).
  • /build-connector-images - Builds and publishes a pre-release docker image for the modified connector(s).
  • /publish-connectors-prerelease - Publishes pre-release connector builds (tagged as {version}-preview.{git-sha}) for all modified connectors in the PR.
  • /ai-review - AI-powered PR review for connector safety and quality gates.
  • /ai-docs-review - AI-powered documentation review for PRs with connector changes.
  • /ai-create-docs-pr - Creates a documentation PR for connector changes.
  • /force-merge reason="<A_GOOD_REASON>" - Force merges the PR using admin privileges, bypassing CI checks. Requires a reason.

Tips for Working with CI

  1. Pre-Release Checks. Please pay attention to these, as they contain standard checks on the metadata.yaml file, docs requirements, etc. If you need help resolving a pre-release check, please ask a maintainer.
    • Note: If you are creating a new connector, please be sure to replace the default logo.svg file with a suitable icon.
  2. Connector CI Tests. Some failures here may be expected if your tests require credentials. Please review these results to ensure (1) unit tests are passing, if applicable, and (2) integration tests pass to the degree possible and expected.
  3. (Optional.) BYO Connector Credentials for tests in your fork. You can optionally set up your fork with BYO credentials for your connector. This can significantly speed up your review, ensuring your changes are fully tested before the maintainers begin their review.
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@Donnype
Donny Peeters (Donnype) marked this pull request as ready for review August 4, 2026 09:43
@airbyte-support-bot

Copy link
Copy Markdown
Contributor

↪️ Triggering /ai-review per Hands-Free AI Triage Project triage next step.

Reason: The PR is ready for review and has no AI review on record yet, so a review pass is the next pipeline gate.

Devin session

@octavia-bot

octavia-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

AI PR Review starting...

Reviewing PR for connector safety and quality.
View playbook

Devin AI session created successfully!

@airbyte-support-bot

Airbyte Support Bot (airbyte-support-bot) commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🛡️ AI PR Review Report

🔴 Review Action: REQUEST CHANGES

Gate Status
CI Checks FAIL
Live / E2E Tests UNKNOWN

🔶 Risk Level: 3/5

Logic change in the destination write path (column-name normalization in _serialize_object_columns) affecting all JSON object/array columns, but scoped to one connector and covered by new unit + integration tests.

🔧 Remediation Required

CI Checks (FAIL)Test destination-motherduck Connector [No Creds] failed on both runs for this SHA:

FAILED .tmp/integration_tests/test_airbyte_standards.py::TestSuite::test_docker_image_build_and_check['config' Test Scenario1]
FileNotFoundError: [Errno 2] No such file or directory:
  airbyte-integrations/connectors/destination-motherduck/secrets/config.json

Unit tests all passed (1 failed, 2 passed, 2 skipped); the single failure is the credentials-dependent standard test. Test failure expected for a community PR without credentials — maintainers can re-run with credentials (/run-connector-tests) or manually approve after code review.

Live / E2E Tests (UNKNOWN) — this is a bug fix touching sync-write behavior, so validation is required, but no /ai-prove-fix conclusion exists on this PR. Run /ai-prove-fix to publish a pre-release and validate the change.


📋 PR Details

Connector(s): destination-motherduck
PR: #83694
HEAD SHA: 3b9aa852f1d469372a5fde5bc233c0f92057b4c3
Session: https://app.devin.ai/sessions/1a599b94c2724c0787b44a3a85b0096c

🔍 Gate Evaluation Details

Gate-by-Gate Analysis

Gate Status Enforced? Details
PR Hygiene PASS Yes Description ~900 chars with What/How/Review guide/User Impact filled in; changelog row for 0.2.6 added to docs/integrations/destinations/motherduck.md; no human review comments outstanding.
Code Hygiene PASS WARNING Source change in processors/duckdb.py accompanied by unit_tests/test_serialize_object_columns.py and integration_tests/integration_test.py.
Test Coverage PASS Yes Bug fix with new test content: new test_serialize_object_columns_normalized_column_names plus a new pytest.param(... id="pascal_case_property_matched_via_normalization") case and new integration assertions.
Code Security PASS Yes No changed file matches auth/credential/token/secret path patterns; metadata.yaml diff is a dockerImageTag bump only (no allowedHosts/connectorBuildOptions/dockerRepository keywords).
Per-Record Performance PASS WARNING The added dict-comprehension normalizes schema property names once per stream flush inside _serialize_object_columns, not per record.
Breaking Dependencies PASS WARNING Only the connector's own version changes (pyproject.toml 0.2.5 → 0.2.6); no third-party dependency versions modified.
Backwards Compatibility PASS Yes (enforced) No spec*.json/spec*.yaml change, no stream/schema/primary-key/cursor change, no format/type change; metadata.yaml diff is a patch dockerImageTag bump with no breaking indicators. Behavior change is strictly corrective (columns that previously failed the load now serialize).
Forwards Compatibility PASS Warning (elevates Risk Level) No state/cursor/checkpoint/partition-router/paginator/transformation keywords in the diff hunks; PR is marked safely revertible.
Behavioral Changes PASS Warning (elevates Risk Level) No rate-limit, retry/backoff, timeout, error-handler, or resource-limit keywords in the diff hunks.
Out-of-Scope Changes PASS Skip All 6 changed files are under airbyte-integrations/connectors/destination-motherduck/ or docs/.
CI Checks FAIL Yes Test destination-motherduck Connector [No Creds] failed (both runs) with FileNotFoundError: .../secrets/config.json. Test failure expected for community PR without credentials — maintainers can manually approve after code review. Lint destination-motherduck Connector, Format Check, Build and Verify Artifacts, Check Changelog Updated, and docs checks all passed. destination-motherduck Progressive Rollout Gate / Connector Active Progressive Rollout Checks Summary failures are release-management checks and are excluded from this gate per playbook.
Live / E2E Tests UNKNOWN Yes Validation required (bug fix + sync-write behavior change in write_stream_data_from_buffer), but no /ai-prove-fix conclusion is posted on this PR. Not run ≠ disproven, so UNKNOWN rather than FAIL.

Change under review: _serialize_object_columns now receives the connector's NameNormalizer and normalizes JSON-schema property names before matching them against buffer keys (which are already normalized column names). Previously, a source property such as Line never matched the buffer key line, so object/array columns from camel/PascalCase sources skipped JSON serialization and reached PyArrow as list<struct<...: struct<>>>, which DuckDB rejects with "Attempted to convert a STRUCT with no fields to DuckDB".

📚 Evidence Consulted

Evidence

  • Changed files: 6 (destination_motherduck/processors/duckdb.py, unit_tests/test_serialize_object_columns.py, integration_tests/integration_test.py, metadata.yaml, pyproject.toml, docs/integrations/destinations/motherduck.md) — +43 / −7
  • CI checks (HEAD 3b9aa85): 23 success, 1 neutral (CodeQL), 11 skipped, 4 failure — failures are Test destination-motherduck Connector [No Creds] ×2 (core, counted), destination-motherduck Progressive Rollout Gate and Connector Active Progressive Rollout Checks Summary (excluded as release-management checks)
  • PR labels: community, connectors/destination/motherduck, hyd-review; no validation/override labels
  • PR description: present (~900 chars, all template sections filled)
  • Existing bot reviews: none for this HEAD SHA
  • /ai-prove-fix conclusion: none found on this PR
❓ How to Respond

Resolving a Failing Gate

Each non-PASS gate above lists concrete remediation. Apply it, push, then re-run /ai-review — the bot re-evaluates from the diff, CI check-runs, and /ai-prove-fix results.

Written explanations do not change a gate verdict. If you believe a gate is a false positive or it cannot be remediated as written, request review from a human maintainer and explain the situation to them in a PR comment — a human reviewer can approve or merge over a bot FAIL. Never edit the PR description to argue with the bot; the description is how reviewers learn what the PR does.

@airbyte-support-bot

Copy link
Copy Markdown
Contributor

🙋 Escalated to #human-in-the-loop per Hands-Free AI Triage Project triage next step.

Reason: /ai-review returned FAIL for the exact current HEAD 3b9aa852 on its first review pass and connector CI is red, so the hands-free pipeline has no further automated gate to run. A maintainer decision is needed on the review findings.

Devin session

@aaronsteers

Aaron ("AJ") Steers (aaronsteers) commented Aug 4, 2026

Copy link
Copy Markdown
Member

/run-connector-tests

Connector CI Tests Started

These tests will leverage Airbyte's integration test credentials.

Check job output.
✅ Connector CI Tests job completed successfully. See logs for details.

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

Fixes a MotherDuck destination edge case where JSON column serialization is skipped when source schema property names are camelCase/PascalCase but buffer keys are normalized (lowercased), which can reintroduce DuckDB “empty STRUCT” failures.

Changes:

  • Normalize JSON schema property keys before deciding whether to JSON-serialize buffered columns.
  • Extend unit/integration tests to cover PascalCase/UpperCase JSON array/object columns.
  • Bump destination-motherduck version to 0.2.6 and document the release.

Reviewed changes

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

Show a summary per file
File Description
docs/integrations/destinations/motherduck.md Adds release note entry for 0.2.6 describing the fix.
airbyte-integrations/connectors/destination-motherduck/unit_tests/test_serialize_object_columns.py Adds regression coverage for normalized vs source-property name mismatch.
airbyte-integrations/connectors/destination-motherduck/pyproject.toml Bumps connector version to 0.2.6.
airbyte-integrations/connectors/destination-motherduck/metadata.yaml Updates Docker image tag to 0.2.6.
airbyte-integrations/connectors/destination-motherduck/integration_tests/integration_test.py Adds an end-to-end regression for an UpperCase array-of-objects JSON field.
airbyte-integrations/connectors/destination-motherduck/destination_motherduck/processors/duckdb.py Normalizes schema property names before JSON-column serialization decisions.
Suppressed comments (2)

airbyte-integrations/connectors/destination-motherduck/unit_tests/test_serialize_object_columns.py:63

  • LowerCaseNormalizer should be instantiated before being passed into _serialize_object_columns (it’s used as LowerCaseNormalizer() in other tests).
    buffer_data = {"id": ["1"], "array_of_objects": [[{}]]}

    serialized = _serialize_object_columns(buffer_data, JSON_SCHEMA, LowerCaseNormalizer)
    pa_table = pa.Table.from_pydict(serialized)

airbyte-integrations/connectors/destination-motherduck/unit_tests/test_serialize_object_columns.py:85

  • LowerCaseNormalizer should be instantiated before being passed into _serialize_object_columns (it’s used as LowerCaseNormalizer() in other tests).
    buffer_data = {"id": ["1"], "line": [[{"Amount": 100.0, "SubTotalLineDetail": {}}]]}

    serialized = _serialize_object_columns(buffer_data, JSON_SCHEMA, LowerCaseNormalizer)
    pa_table = pa.Table.from_pydict(serialized)

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

Comment on lines 39 to 43
def _serialize_object_columns(
buffer_data: Dict[str, List[Any]],
json_schema: dict,
normalizer: type[NameNormalizerBase],
) -> Dict[str, List[Any]]:

@aaronsteers Aaron ("AJ") Steers (aaronsteers) Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC, I think this class is designed to work either way - with class methods and optional instance properties.

It may be fine as-is, or optionally as:

Suggested change
def _serialize_object_columns(
buffer_data: Dict[str, List[Any]],
json_schema: dict,
normalizer: type[NameNormalizerBase],
) -> Dict[str, List[Any]]:
def _serialize_object_columns(
buffer_data: Dict[str, List[Any]],
json_schema: dict,
normalizer: type[NameNormalizerBase] | NameNormalizerBase,
) -> Dict[str, List[Any]]:

Comment on lines 47 to 49
def test_serialize_object_columns(col_name, values, expected) -> None:
result = _serialize_object_columns({col_name: values}, JSON_SCHEMA)
result = _serialize_object_columns({col_name: values}, JSON_SCHEMA, LowerCaseNormalizer)
assert result[col_name] == expected

@aaronsteers Aaron ("AJ") Steers (aaronsteers) Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ditto my last comment. Passing the class is correct and expected.

@aaronsteers

Aaron ("AJ") Steers (aaronsteers) commented Aug 4, 2026

Copy link
Copy Markdown
Member

/ai-prove-fix

AI Prove Fix Started

Running readiness checks and testing against customer connections.
View workflow run
🔍 AI Prove Fix session starting... Running readiness checks and testing against customer connections. View playbook

Devin AI session created successfully!

@airbyte-support-bot

Airbyte Support Bot (airbyte-support-bot) commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

✅ Fix Proven — destination-motherduck 0.2.6-preview

Bottom line: A production connection that was failing on 100% of sync attempts with Invalid Input Error: Attempted to convert a STRUCT with no fields to DuckDB synced successfully on its first attempt after being pinned to the pre-release build of this PR. The fix works.

Evidence summary (sanitized)

  • Connector / pre-release: airbyte/destination-motherduck:0.2.6-preview.3b9aa85 (publish workflow, succeeded)
  • Baseline: current release 0.2.5 — the same connection had 16 consecutive failed attempts in one job, every one failing during the destination write of a stream containing PascalCase object columns (pyarrow buffer showed struct<> for a PascalCase source property — the exact code path this PR fixes)
  • Test: with human approval, the destination actor was pinned to the pre-release; the next sync attempt ran with the pre-release image (confirmed in logs) and succeeded — destination logs show "Starting write to DuckDB with 5 streams" → "Writing complete.", zero failures, all records committed across all 5 streams including the previously-failing one
  • No regressions observed: no new destination errors on any stream
  • Detailed (customer-identifying) evidence: internal issue

Pre-flight checks — PASS: non-breaking patch bump (0.2.5→0.2.6), no schema/spec/state changes, reversible, changelog present, no safety concerns.

Next steps

  1. This PR can proceed to review/merge from a validation standpoint.
  2. The test connection remains pinned to the pre-release (it cannot sync at all on 0.2.5); the pin should be removed once 0.2.6 is published.
Process log
  • Initial status posted
  • Pre-flight checks (viability, safety, breaking change, reversibility) — PASS
  • Pre-release published: 0.2.6-preview.3b9aa85
  • Evidence plan posted
  • HITL approval obtained for live connection pin
  • Live test executed — sync succeeded on pre-release after repeated failures on 0.2.5
  • Detailed results posted to private issue

Evidence plan (as executed)

  • Strategy: live connection test (regression tests support sources only; this is a destination connector)
  • Proving criteria: previously-failing sync (empty-STRUCT error from camel/PascalCase object columns) succeeds on the pre-release → met
  • Disproving criteria: same error persists or new write errors appear → not observed

Devin session

@airbyte-support-bot Airbyte Support Bot (airbyte-support-bot) added the hyd-prove Hydra: ai-prove-fix stage has run label Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Pre-release Connector Publish Started

Publishing pre-release build for connector destination-motherduck.
PR: #83694

Pre-release versions will be tagged as {version}-preview.3b9aa85
and are available for version pinning via the scoped_configuration API.

View workflow run
Pre-release Publish: SUCCESS

Docker image (pre-release):
airbyte/destination-motherduck:0.2.6-preview.3b9aa85

Docker Hub: https://hub.docker.com/layers/airbyte/destination-motherduck/0.2.6-preview.3b9aa85

Registry JSON:

@aaronsteers
Aaron ("AJ") Steers (aaronsteers) merged commit c3bbd19 into airbytehq:master Aug 4, 2026
53 of 58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

5 participants