Skip to content

Say which column default is invalid when creating a table fails - #9013

Merged
andypalmi merged 5 commits into
mainfrom
8933-tables-invalid-column-default
Oct 22, 2026
Merged

andypalmi merged 5 commits into
mainfrom
8933-tables-invalid-column-default

Conversation

@cstns

@cstns cstns commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Creating an FF Tables table with a default that doesn't fit its column type failed with a bare "Failed to create table", so there was no way to tell which column or value to fix. Dates like not-a-date failed in Postgres with a message that names the value but not the column. Numbers that parseInt or parseFloat can't read went into the SQL as NaN or Infinity and failed with an unrelated "cannot use column reference in DEFAULT expression".

The create-table route now returns a 400 invalid_column_default naming the column and value, e.g. Invalid default value "not-a-date" for date column "created". Bad numeric defaults are caught before the driver runs. A date Postgres rejects is matched back to its column from the quoted value in the error, since the route can't tell up front which of the many date formats Postgres accepts. Anything that works today still does (today, 01/02/2026, a bigint of 12abc), and any other failure keeps the generic 500. The UI and the MCP tool both go through this route, so they pick up the message without changes.

Checked against the local Postgres through the running forge: bad dates, out of range dates, a bad bigint and an Infinity real all come back as the new 400, and the defaults above still create tables. In the Create Table drawer the toast now shows the column and value. Route tests cover both paths and the generic fallback.

Closes #8933

A numeric default that parseInt or parseFloat can't read is now rejected up front with a 400 that names the column and the value, instead of reaching Postgres as NaN or Infinity and failing with an unrelated error. A date default Postgres rejects is matched back to its column from the quoted value in the error, so it gets the same 400. Defaults that work today are left alone.
@cstns cstns self-assigned this Oct 21, 2026
@cstns
cstns requested review from andypalmi and hardillb October 21, 2026 11:55
@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.54%. Comparing base (0690f91) to head (019d8ee).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9013      +/-   ##
==========================================
+ Coverage   79.50%   79.54%   +0.03%     
==========================================
  Files         475      475              
  Lines       26052    26076      +24     
  Branches     6942     6950       +8     
==========================================
+ Hits        20712    20741      +29     
+ Misses       5340     5335       -5     
Flag Coverage Δ
backend 79.54% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andypalmi andypalmi 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.

  1. Small edge in findInvalidNumericDefault: a very large bigint default like 99999999999999999999999 passes the check, because parseInt returns 1e+23. The driver then writes DEFAULT 1e+23 and Postgres rejects it as out of range, so the user still gets the bare "Failed to create table". Would it make sense to also reject values that aren't safe integers (Number.isSafeInteger)? Anything that works today would keep working.

  2. Question on findInvalidDateDefault: it matches on the quoted value, so if two date columns share the same bad default, the message names the first one. Is that fine for now, since the user fixes it and retries, or worth naming both?

parseInt rounds a large bigint default into a value Postgres stores without complaint, so the table gets created and every insert that relies on the default then fails with bigint out of range. Such defaults now get the same 400 naming the column and value, before the driver runs. Values that only lose precision still create the table as they do today.
@cstns

cstns commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on the big bigints, thanks. It turned out a bit worse than expected: I tried it on Postgres and DEFAULT 1e+23 doesn't actually fail the create. The table gets created fine and then every insert that relies on that default fails with bigint out of range. Even the exact min and max bigint hit this, since parseInt rounds them just past the limit.

I went with a range check instead of Number.isSafeInteger, so anything outside the bigint range now gets the same 400 naming the column, and no table is created. isSafeInteger would also have rejected values like 9007199254740993, which get rounded a little but create and insert fine today, so I left those alone to keep the "works today still works" promise. Tests cover both sides.

On the two date columns sharing a bad default, I think naming the first is fine for now. Postgres stops at the first failure anyway, so with different bad values the user already fixes them one at a time, and with the same value the second one gets named on the retry.

@andypalmi

Copy link
Copy Markdown
Contributor
  1. Thanks, the range check is a better call than isSafeInteger, and keeping 9007199254740993 working makes sense.

  2. One gap at the lower bound in findInvalidNumericDefault: parseInt('-9223372036854775808') is exactly -(2 ** 63), so value < -(2 ** 63) lets it through. The driver then writes ${value}, which prints as -9223372036854776000, one past the bigint minimum, so the same insert failure happens there. Would value <= -(2 ** 63) cover it? A test with -9223372036854775808 next to the max one would pin it.

  3. Agreed on the date columns, naming the first is fine.

Comment thread forge/ee/routes/tables/index.js Outdated
@andypalmi
andypalmi enabled auto-merge (squash) October 22, 2026 12:38
@andypalmi
andypalmi merged commit 107cc8c into main Oct 22, 2026
29 checks passed
@andypalmi
andypalmi deleted the 8933-tables-invalid-column-default branch October 22, 2026 13:02

This branch was successfully deployed

1 active deployment
staging — 019d8eef Deployed Oct 22, 2026 by andypalmi via Remove application #12693
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.

Creating a table with an invalid column setting gives a generic error

2 participants