Skip to content

fix(tables): coerce row values to column types on write instead of failing#4761

Merged
TheodoreSpeaks merged 3 commits into
stagingfrom
fix/enrich-data-cast-error
May 27, 2026
Merged

fix(tables): coerce row values to column types on write instead of failing#4761
TheodoreSpeaks merged 3 commits into
stagingfrom
fix/enrich-data-cast-error

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

Summary

  • Tool/enrichment writes (e.g. Wiza returning "unknown" for a numeric founding year) no longer fail the entire row write on a single off-type field
  • Added coerceRowToSchema at the Tables write boundary — coerces each cell toward its column's declared type ("1999"1999, 12345"12345", "false"false, epoch → ISO date), nulls un-coercible values on optional columns, and still fails loudly on required columns
  • Routed every write path (route-level validation + all 7 service write callsites) through it; validateRowAgainstSchema stays pure as the final gate

Type of Change

  • Bug fix

Testing

Added coerceRowToSchema unit coverage (8 cases). Full lib/table suite passes (145 tests). Lint + check:api-validation:strict pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 27, 2026 9:32pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 27, 2026

PR Summary

Medium Risk
Changes data persistence semantics for off-type values (optional fields become null) across all table write paths; required-field and unique constraints still enforced after coercion.

Overview
Table writes now coerce cell values toward declared column types before validation, instead of rejecting the whole row when one field is off-type (e.g. enrichment returning "unknown" for a number).

validation.ts adds coerceRowValues (in-place, patch-safe) and coerceRowToSchema (coerce + validateRowAgainstSchema). Safe conversions include numeric strings → numbers, numbers/booleans → strings, "true"/"false" → booleans, and dates → ISO strings; optional columns get null when coercion fails; required columns still fail validation.

service.ts and route-level validateRowData / validateBatchRows route inserts, updates, batch paths, and merged-row validation through coerceRowToSchema. Bulk filter updates call coerceRowValues on the patch before persisting. Upsert reads the conflict-target value after coercion so JSONB matching uses the stored type.

Unit tests cover coercion behavior; service test mocks include the new exports.

Reviewed by Cursor Bugbot for commit 42dd8e2. Bugbot is set up for automated code reviews on this repo. Configure here.

@TheodoreSpeaks TheodoreSpeaks force-pushed the fix/enrich-data-cast-error branch from b737c39 to fe6ea03 Compare May 27, 2026 17:37
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR adds a coercion layer (coerceRowToSchema / coerceRowValues) at every table write boundary so that off-type but unambiguously convertible values (e.g. "1999" for a number column, epoch integers for a date column) are coerced in-place rather than causing the entire row write to fail.

  • validation.ts: Three new functions — coerceValueToColumnType (private), coerceRowValues (exported, partial-patch safe), and coerceRowToSchema (exported, full-row entry point that coerces then delegates to validateRowAgainstSchema).
  • service.ts: All 7 write callsites and both route-level validators (validateRowData, validateBatchRows) switched from validateRowAgainstSchema to coerceRowToSchema; upsertRow conflict-target check moved to after coercion; updateRowsByFilter pre-coerces data.data before the loop because only the patch is persisted via a SQL JSONB merge.
  • Tests: 11 coerceRowToSchema cases and 3 coerceRowValues cases added; mock stubs updated in the two service test files.

Confidence Score: 5/5

Safe to merge — the coercion layer is additive and every write path is covered.

The change is well-scoped: coercion is applied in-place before the existing validateRowAgainstSchema gate, so required-field enforcement and uniqueness checks are unchanged. Out-of-range epochs and invalid Date instances are correctly guarded via Number.isNaN(date.getTime()). The updateRowsByFilter split (pre-coerce patch, validate merged copy) is correctly handled, and the upsertRow reordering is logically sound. Test coverage for coercion is thorough.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/lib/table/validation.ts Adds coerceValueToColumnType (private), coerceRowValues (exported), and coerceRowToSchema (exported). All existing write paths are routed through coerceRowToSchema. Route-level validators validateRowData and validateBatchRows updated accordingly. Implementation is correct; edge cases (out-of-range epochs, invalid Dates, NaN) are properly guarded.
apps/sim/lib/table/service.ts All 7 write callsites switched from validateRowAgainstSchema to coerceRowToSchema. updateRowsByFilter correctly pre-coerces data.data before the loop (since only the patch is persisted via JSONB merge, not mergedData). upsertRow target-value check correctly moved to after coercion. updateRow and batchUpdateRows coerce and re-write the full merged row, consistent with their existing full-row write semantics.
apps/sim/lib/table/tests/validation.test.ts Adds 11 unit tests covering coerceRowToSchema and 3 tests for coerceRowValues on partial patches, including tricky edge cases like out-of-range epochs and invalid Date instances.
apps/sim/lib/table/tests/service-filter-threading.test.ts Mock updated to expose coerceRowToSchema and coerceRowValues stubs, keeping existing filter/threading tests passing.
apps/sim/lib/table/tests/update-row.test.ts Mock updated to expose coerceRowToSchema and coerceRowValues stubs alongside the existing update-row test suite.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming row data] --> B[validateRowSize]
    B -->|invalid| ERR1[Throw / return error]
    B -->|valid| C[coerceRowToSchema]
    C --> D[coerceRowValues — mutates data in-place]
    D --> E{per column}
    E -->|value already matches type| F[leave untouched]
    E -->|coercible mismatch| G[apply coercion\ne.g. '1999' → 1999]
    E -->|un-coercible + optional| H[set to null]
    E -->|un-coercible + required| I[leave in-place for validator]
    F & G & H & I --> J[validateRowAgainstSchema]
    J -->|errors| ERR2[Throw / return error]
    J -->|valid| K[unique constraint check]
    K -->|violation| ERR3[Throw / return error]
    K -->|ok| L[DB write]
Loading

Reviews (2): Last reviewed commit: "fix(tables): guard date coercion against..." | Re-trigger Greptile

Comment thread apps/sim/lib/table/validation.ts Outdated
Comment thread apps/sim/lib/table/service.ts
Comment thread apps/sim/lib/table/service.ts
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

Addressed all three review findings in 4f72933:

  • [cursor, High] Bulk update wrote un-coerced patchupdateRowsByFilter validated discarded per-row mergedData copies but persisted JSON.stringify(data.data). Now coerces data.data in place (new coerceRowValues, a coercion-only pass that's safe on a partial patch — it never invents a missing-required-field error) before building patchJson / the returned rows. The merged-row loop still enforces required fields.
  • [cursor, Medium] Stale targetValue in upsert — moved the conflict-target read to after coerceRowToSchema, so matchFilter branches on the coerced/persisted type (e.g. "123"123 now matches existing rows).
  • [greptile, P2] date coercion returned a Date instance — now normalizes to toISOString(), matching every other branch (always a serializable primitive).

Verified updateRow (writes mergedData) and batchUpdateByIds (writes coerced merged) already persist the coerced object — no change needed there. Added unit coverage for coerceRowValues partial-patch behavior and Date → ISO. Full lib/table suite green (154 tests), lint + typecheck pass.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4f72933. Configure here.

Comment thread apps/sim/lib/table/validation.ts Outdated
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

Addressed the new finding in 42dd8e2:

  • [cursor, Medium] RangeError from date coercion of out-of-range valuesnew Date(value).toISOString() threw on values beyond the ±8.64e15ms Date range (and on an invalid Date instance), surfacing as a 500. Now the date branch builds the Date, checks Number.isNaN(date.getTime()), and degrades to { ok: false } for an over-range/invalid value — so it nulls the cell (optional column) or fails validation cleanly (required column) instead of crashing. Added coverage for an out-of-range epoch and an invalid Date instance.

The other three threads (bulk-update write, stale upsert targetValue, Date → ISO) are re-surfaced from the earlier commit fe6ea03 and were already fixed in 4f72933.

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks TheodoreSpeaks merged commit 41c8193 into staging May 27, 2026
14 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the fix/enrich-data-cast-error branch May 27, 2026 23:19
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.

1 participant