8000 feat(postgres): read bounds, a divisor and a pattern from a check by colinhacks · Pull Request #200 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

feat(postgres): read bounds, a divisor and a pattern from a check - #200

Open
colinhacks wants to merge 2 commits into
mainfrom
check-facts
Open

colinhacks wants to merge 2 commits into
mainfrom
check-facts

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

The Postgres engine now reads three more facts from a CHECK into a column's type: a number's bounds, its divisor and a string's pattern. These fields were in the format, but no engine wrote them.

Declared Type
age int check (age between 0 and 150) { "kind": "integer", "bits": 32, "min": "0", "max": "150", "name": "int4" }
pos int check (pos > 0) { "kind": "integer", "bits": 32, "min": "1", "name": "int4" }
rate numeric check (rate > 0 and rate < 1) "exclusive_min": "0", "exclusive_max": "1"
qty int check (qty % 5 = 0) "multiple_of": "5"
slug varchar(20) check (slug like 'a%') "pattern": { "dialect": "like", "source": "a%" }
code text check (code ~ '^[a-z]+$') "pattern": { "dialect": "postgres", "source": "^[a-z]+$" }
note text check (note ~* 'abc') "pattern": { "dialect": "postgres", "source": "(?i)abc" }

A domain's CHECK counts the same way, and so does a field read through a table's whole row. A bare parameter written to such a column takes these facts too. TypeScript types and .run() don't change.

What it reads

  • Each conjunct of an AND, and the one test beside column is null under an OR.
  • Comparisons of the bare column with a plain number, on int2, int4, int8, floats and numeric.
  • column % n = 0. A divisor of 1 says nothing and is left out.
  • like, ilike, ~ and ~* with a constant, on text or varchar under a deterministic collation.
  • The built-in operators only, resolved as the existing CHECK rules resolve them.

What it leaves out

  • A cast of the column or an expression over it, not, or any other or, which may not bind every row.
  • A constant written with an exponent.
  • A bound the type already implies, such as smallint check (x >= -40000) or one past a numeric(p,s)'s reach.
  • A CHECK no value can meet (x > 5 and x < 1).
  • char(n)'s pattern, since its padding is read back.
  • A column read through a view or an expression.
  • The other databases, which still write none of these.

Tests

  • oracle/cases/check_facts is new: 11 statements, all of which run. The oracle now holds every value to its bounds, divisor and like or ilike pattern. A regular expression is left to the server, which already refused any row it doesn't match.
  • 4 other cases' expected.txt change, 10 lines, each only gaining a fact from a CHECK the case declares.
  • A new engine test covers what each form of CHECK gives.
  • CHECKS_QUERY now reads integer columns. tests/catalog-queries.test.ts holds it to the same answers under a hostile search path.

Verified

  • cargo build --workspace --all-targets with no warning; the Postgres engine's tests (204) and core's.
  • The oracle's 102 Postgres cases with --strict-coverage: none failed.
  • nub run typecheck, and nub run test against Postgres 18 and MySQL 8.4 on the release binary: 365 passed, none skipped.
  • nub run capture in site/ writes the same files.

Copilot AI balanced review requested due to automatic review settings November 6, 2026 17:20
@vercel
vercel Bot commented Nov 6, 2026 •
Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
reylo Ignored Ignored Preview Nov 6, 2026 5:45pm UTC

Request Review

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@pullfrog pullfrog Bot 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.

Important

Two ways the new CHECK reading can produce a descriptor that is wrong: a cast that rounds its constant gives an unsound bound, and an integer CHECK no value can meet gives a minimum above its maximum. Both are inline.

Reviewed changes

I reviewed the whole PR: the Postgres engine reading bounds, a divisor and a pattern from a CHECK, how those reach columns, whole rows and written parameters, and the oracle checks that come with it.

  • check_facts — parses the CHECK text the server prints. It reads each AND conjunct and the one test beside column IS NULL, and only uses built-in operators, resolved through Trust.
  • CheckFacts — combines bounds by taking the tighter one, and leaves out a bound the type's width or numeric(p,s) already implies.
  • Plumbing — the facts follow the same paths non_finite already uses: bare result columns, JSON shapes, stored whole-row fields, and bare written parameters. CHECKS_QUERY now covers int2, int4 and int8.
  • Oracle — fact_contradiction holds seeded values to their bounds, divisor and like/ilike pattern. The new check_facts case is added, and four other expected.txt files gain facts.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/postgres/src/postgres.rs Resolved
Comment thread engine/postgres/src/types.rs Resolved
A cast can round its constant, and a real compares with a double precision widened, so a stored value can pass past the constant the check prints. An integer check no value meets now gives no bounds once its exclusive bounds are made inclusive.
@pullfrog pullfrog Bot 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.

✅ No new issues found. Both earlier findings are fixed, and each has a test.

Reviewed changes

I reviewed the one commit since the last review, 12c54ee. It stops reading a bound when a cast or a float's width changes the constant the CHECK prints, and it leaves out integer bounds that no value can meet.

  • Rounding casts: number() reads through a cast only when the cast keeps its value. To an integer type, the constant has to be whole. To real or double precision, it has to print back as itself. From real to double precision, real has to hold it exactly. A cast from a float to numeric, or to a type with a modifier (numeric(3,1)), gives no bound.
  • Widened comparisons: a real column checked against a double precision constant, or the reverse, gives a bound only when real holds the constant exactly. So real 0.1 passing > 0.1 gives no bound.
  • Unmeetable bounds: CheckFacts::apply leaves the bounds out when they are equal and either one is exclusive. The integer arm now checks after turning exclusive bounds into inclusive ones, against the type's own range, so x > 5 and x < 6 and x > 2147483647 on int4 give no bounds.
  • Tests: new engine assertions cover each form. The new rounding statement in oracle/cases/check_facts seeds the values that broke the old bounds (2.9 under <= 2.5::int, real 0.1 under > 0.1).

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

This branch has not been deployed

No deployments
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

0