# 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.

| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | `c.<col>.matches(/re/)` refuses an unportable construct | your 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.

| Construct | Measured on 18.4 | Write instead |
|---|---|---|
| `\b` | `'foo' ~ '\bfoo'` **false** — ARE reads BACKSPACE | a 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` `\Z` | anchor in ARE, a letter in JS | `^` / `$` |
| `\x` | `'Д' ~ '^\x414$'` **true** — ARE takes 3 hex, JS 2 | a predicate |
| leading `]` in a class | `[]a]` differs | `\]` |
| `\1`–`\9`, `(?<name>…)`, `(?i)`, `[[:alpha:]]`, non-ASCII range endpoint | backreference, named group, inline flag, POSIX class, collation-ordered range | a 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 `'٣'`/`'５'` 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.
