Ultimate

15.x → 16.0.0, entry by entry

One breaking entry, and it fires at entity() — before a migration exists, before anything reaches a database. Nothing migrates, and an app whose patterns are already portable emits exactly what it emitted at 15.x.

#SurfaceCosts you an edit if
1c.<col>.matches(/re/) refuses an unportable constructyour pattern uses \b, ., \w, \s, a backreference, a named group, an inline flag, a POSIX class, \A/\Z, \x, a leading ] in a class, or a non-ASCII range endpoint

1. matches(/re/) refuses what the two engines read differently

At 15.x this accepted any RegExp and emitted a maybe-equivalent POSIX pattern. That is the defect, not the refusal: matches(/\bfoo/) shipped a CHECK that compiled cleanly, errored nowhere, and enforced a BACKSPACE — measured on 18.4, 'foo' ~ '\bfoo' is FALSE while /\bfoo/.test('foo') is true. Two rules under one name, with nothing anywhere to report it.

Nothing is translated now either, and that is the mechanism: pattern.source is the string .test() runs and the string spliced into the constraint. That is only safe because every construct where the engines disagree is refused at declaration.

How to find them: run your entities — bun test, or x db gen. The refusal is at entity() time, so one run surfaces every one, and each names the construct, its index, both readings, and the repair.

ConstructMeasured on 18.4Write instead
\b'foo' ~ '\bfoo' false — ARE reads BACKSPACEa predicate: matches((v) => /\bfoo/.test(v)), app-only, sql: null
.'a\nb' ~ 'a.b' true, JS false[^\n\r]
\w \W'é' ~ '^\w$' true — locale alnum class[A-Za-z0-9_]
\s \S'\u00a0' ~ '^\s$' false, JS true[ \t\n\r\f\v]
\A \Zanchor in ARE, a letter in JS^ / $
\x'Д' ~ '^\x414$' true — ARE takes 3 hex, JS 2a predicate
leading ] in a class[]a] differs\]
\1–\9, (?<name>…), (?i), [[:alpha:]], non-ASCII range endpointbackreference, named group, inline flag, POSIX class, collation-ordered rangea predicate

Kept, and each measured to AGREE: literals, ^ $ | ( ) * + ?, lazy quantifiers, {n}/{n,}/{n,m}, (?: (?= (?! (?<= (?<!, \d \D \n \r \t \f \v, punctuation escapes, bracket expressions with ASCII ranges, non-ASCII members, and \uwxyz at exactly four hex digits.

Two of those earn a note. \d is in by measurement, not by reading — POSIX pins [[:digit:]] at the ten ASCII digits, and '٣'/'5' are false on both sides. \uwxyz is in because Bun escapes a regex literal's non-ASCII characters (/^é$/.source is ^\u00E9$), so refusing it would have refused every i18n pattern written the ordinary way.

The subset is falsifiable, not asserted. One live test runs every refused construct against a real server and asserts the two engines still disagree; another runs the kept subset and asserts they agree. A future Postgres that grows JavaScript's \b turns those red rather than leaving a stale exclusion in place.

Not breaking, but you will see it

x db gen now emits drop index / drop constraint for a recorded index no entity declares. At 15.x it emitted nothing, so those objects stayed on the database while the next sidecar stopped recording them — and drift was green over it. Your first generation after upgrading may carry drops you did not expect; read them, they are objects your entities genuinely no longer declare. A recorded unique CONSTRAINT gets a drop constraint … if exists before the drop index, because drop index on a constraint-backed index is 2BP01 and if exists does not suppress it.

Edit this page on GitHubThis page as markdown