New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/entity

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/entity - npm Package Compare versions

Comparing version
15.0.0
to
16.0.0
+15
src/is-null.ts
// Single responsibility: what this package means by NULL when it looks at a row. One rule, because
// three files had their own copy of it — `memory-match.ts`, `containment.ts` and, the moment
// `isNull()` joined the invariant vocabulary, `expr.ts`.
/**
* Absent and NULL are one value. `undefined` is a key nobody typed or a column a projection left
* out; `null` is one the row spelled. Postgres cannot tell them apart and neither may anything
* reading a row here.
*
* A copy is not free: the rule decides whether a row the caller never NAMED a column on is the same
* row as one that stored `null`, and the table holds NULL for both. `===` made them two, and
* `eq null` then skipped the absent row while `neq null` answered it — the opposite of the same
* predicate in production.
*/
export const isNullish = (value: unknown): boolean => value === null || value === undefined;
// Single responsibility: decide whether every construct in a regex source means the SAME thing to
// `RegExp.prototype.test` and to Postgres' `~`. Nothing here escapes, emits or refuses — it names
// the first construct that does not, and `expr.ts` turns that into the refusal.
//
// WHY it has to exist: `matches(/…/)` puts ONE string in front of two engines, and JavaScript's
// RegExp and Postgres' ARE are not the same language. `\b` is a word boundary in one and a
// BACKSPACE character in the other; both compile, neither errors, and the CHECK enforces a rule the
// app never wrote. Refusing the construct is the only outcome that keeps "one declaration, two
// enforcement points" true.
/** The first construct in a pattern the two engines disagree about. */
export interface UnportablePattern {
/** As it appears in the source: `\b`, `.`, `[:`, `a-é`. */
readonly construct: string;
/** Offset of `construct` in the source, so the refusal can point at it. */
readonly at: number;
/** One sentence naming BOTH readings — never "unsupported". */
readonly why: string;
/** A spelling that means the same thing in both, when one exists. */
readonly instead: string | undefined;
}
/** A cursor past the construct just read, or the refusal that ends the scan. */
type Step = number | UnportablePattern;
const isRefusal = (step: Step): step is UnportablePattern => typeof step !== 'number';
const refuse = (
construct: string,
at: number,
why: string,
instead: string | undefined,
): UnportablePattern => ({ construct, at, why, instead });
/**
* `\` + one of these is the same character class or control character in both engines. `\d` earns
* its place by measurement, not by reading: POSIX fixes `[[:digit:]]` at the ten ASCII digits in
* every locale, and `'٣' ~ '^\d$'` and `'5' ~ '^\d$'` are both false on a UTF-8 server, exactly as
* `/^\d$/` is. `\w` and `\s` are the two that look like they belong here and do not.
*/
const PORTABLE_ESCAPES = new Set(['d', 'D', 'n', 'r', 't', 'f', 'v']);
/**
* The escapes that compile on both sides and mean different things. Measured on PostgreSQL 18.4,
* UTF8, `en_US.utf8` — the pairs are in `pg-invariant-pattern.live.test.ts`, which re-runs them
* against whatever server is configured so a future Postgres cannot quietly change one.
*
* A `Map` and not a frozen record: the key is data read from a pattern, and `TABLE[key]` on an
* object literal answers an `Object.prototype` member for `constructor` and `toString`.
*/
const DIVERGENT_ESCAPES = new Map<string, readonly [why: string, instead: string | undefined]>([
['b', ['is a word boundary in JavaScript and a BACKSPACE character in Postgres', undefined]],
['B', ['is a non-word-boundary in JavaScript and a literal backslash in Postgres', undefined]],
[
'w',
[
"is [A-Za-z0-9_] in JavaScript and the LOCALE's alphanumeric class in Postgres, which matches é",
'[A-Za-z0-9_]',
],
],
[
'W',
[
"is [^A-Za-z0-9_] in JavaScript and the complement of the LOCALE's alphanumeric class in Postgres",
'[^A-Za-z0-9_]',
],
],
[
's',
[
'matches U+00A0 and the Unicode separators in JavaScript, which Postgres’ [[:space:]] does not',
'[ \\t\\n\\r\\f\\v]',
],
],
['S', ['is the complement of a class the two engines do not agree on', '[^ \\t\\n\\r\\f\\v]']],
['A', ['anchors the start of the string in Postgres and is the letter A in JavaScript', '^']],
['Z', ['anchors the end of the string in Postgres and is the letter Z in JavaScript', '$']],
['y', ['is a word boundary in Postgres and the letter y in JavaScript', undefined]],
['Y', ['is a non-word-boundary in Postgres and the letter Y in JavaScript', undefined]],
['m', ['anchors a word start in Postgres and is the letter m in JavaScript', undefined]],
['M', ['anchors a word end in Postgres and is the letter M in JavaScript', undefined]],
['a', ['is BEL in Postgres and the letter a in JavaScript', undefined]],
['e', ['is ESC in Postgres and the letter e in JavaScript', undefined]],
['x', ['reads up to three hex digits in Postgres and exactly two in JavaScript', undefined]],
['U', ['is an 8-digit codepoint in Postgres and the letter U in JavaScript', undefined]],
['c', ['is a control escape the two engines delimit differently', undefined]],
['k', ['names a group Postgres cannot declare', undefined]],
['p', ['is a Unicode property in JavaScript and an error in Postgres', undefined]],
['P', ['is a negated Unicode property in JavaScript and an error in Postgres', undefined]],
['0', ['is NUL in JavaScript and no Postgres text can hold one', undefined]],
]);
const BACKREFERENCE = [
'is a backreference, and the two engines number their groups differently once a lookaround is',
'involved',
].join(' ');
/**
* The group openings that survive both engines. `(?:` `(?=` `(?!` `(?<=` `(?<!` are measured to
* agree — each has a row in `pg-invariant-pattern.live.test.ts`'s agreement table, the two
* LOOKBEHINDS only since 2026-08-25: this sentence shipped naming five and the table ran three, so
* it was broader than the evidence for as long as it existed. The whole rest of the `(?` family is
* refused, because ARE has no named groups at all — `(?<year>…)` is a server-side `invalid regular
* expression` — and its inline directors (`(?i)`) are not JavaScript syntax.
*
* Greediness needs no rule: `~` and `.test()` both answer whether a match EXISTS, and with
* backreferences refused no amount of greedy-vs-lazy backtracking can change that answer. So
* `a+?b` stays in.
*/
const GROUP_OPENINGS = ['(?:', '(?=', '(?!', '(?<=', '(?<!'] as const;
/** `{n}` `{n,}` `{n,m}`, the three forms both engines read the same way. */
const QUANTIFIER = /^\{\d+(?:,\d*)?\}/;
/**
* `\uwxyz` is EXACTLY four hex digits in ARE and exactly four in JavaScript without the `u` flag —
* measured to agree, including inside a bracket expression. It is in the subset for a reason that
* is not convenience: **Bun returns a regex LITERAL's non-ASCII characters escaped**,
* `/^é$/.source` is `^\u00E9$`, so refusing the escape would refuse every pattern an i18n rule
* writes. Anything shorter is `invalid escape` on the server and the letter `u` in JavaScript.
*/
const HEX4 = /^[0-9A-Fa-f]{4}/;
const isAscii = (char: string): boolean => (char.codePointAt(0) ?? 0) < 0x80;
const NUL = refuse(
'\\0',
0,
'is a null byte, which no Postgres text value can hold — the statement never reaches the server',
undefined,
);
function escapeAt(source: string, at: number): Step {
const next = source[at + 1];
if (next === undefined) {
return refuse('\\', at, 'ends the pattern, so there is nothing for it to escape', undefined);
}
const divergent = DIVERGENT_ESCAPES.get(next);
if (divergent !== undefined) return refuse(`\\${next}`, at, divergent[0], divergent[1]);
if (next === 'u') {
return HEX4.test(source.slice(at + 2))
? at + 6
: refuse(
'\\u',
at,
'is a codepoint escape Postgres reads as exactly four hex digits and JavaScript reads as the letter u when there are fewer',
undefined,
);
}
if (PORTABLE_ESCAPES.has(next)) return at + 2;
if (next >= '1' && next <= '9') return refuse(`\\${next}`, at, BACKREFERENCE, undefined);
// Postgres reads `\` + any remaining ALPHANUMERIC as a special it has not been taught here, and
// JavaScript reads it as the letter — the `\b` shape, one letter along. `\` + punctuation is that
// character literally in both, which is what keeps `\.` `\$` `\-` `\'` `\]` in the subset.
if (/[0-9A-Za-z]/.test(next)) {
return refuse(
`\\${next}`,
at,
'is an escape Postgres reads as a special and JavaScript reads as the letter',
undefined,
);
}
if (!isAscii(next) || next < ' ') {
return refuse(
`\\${next}`,
at,
'escapes a character outside printable ASCII, where the two engines are not measured to agree',
undefined,
);
}
return at + 2;
}
function groupAt(source: string, at: number): Step {
if (source[at + 1] !== '?') return at + 1;
const opening = GROUP_OPENINGS.find((form) => source.startsWith(form, at));
if (opening !== undefined) return at + opening.length;
return refuse(
source.slice(at, at + 4),
at,
'is a group form Postgres has no syntax for — it has no named groups and no inline flags',
undefined,
);
}
const LEADING_BRACKET_WHY =
'opens a class whose first ] Postgres reads as a MEMBER and JavaScript reads as the close of an empty class';
const RANGE_WHY =
'is a range whose endpoints Postgres orders by the database COLLATION and JavaScript orders by code point';
function bracketAt(source: string, at: number): Step {
let cursor = source[at + 1] === '^' ? at + 2 : at + 1;
// `[]a]` is the literal `]` plus `a` to Postgres and an EMPTY class followed by `a]` to
// JavaScript — measured to disagree, and `\]` is the one spelling both read as a member.
if (source[cursor] === ']') return refuse('[]', at, LEADING_BRACKET_WHY, '\\]');
/** The last member that could be the lower end of a range; `undefined` after one is consumed. */
let previous: string | undefined;
while (cursor < source.length) {
const char = source[cursor] ?? '';
if (char === ']') return cursor + 1;
if (char === '\u0000') return { ...NUL, at: cursor };
if (char === '[') {
const kind = source[cursor + 1] ?? '';
if (kind === ':' || kind === '.' || kind === '=') {
return refuse(
`[${kind}`,
cursor,
'opens a POSIX class, collating element or equivalence class, none of which JavaScript has',
undefined,
);
}
previous = char;
cursor += 1;
continue;
}
if (char === '\\') {
const step = escapeAt(source, cursor);
if (isRefusal(step)) return step;
previous = undefined;
cursor = step;
continue;
}
const upper = source[cursor + 1];
if (char === '-' && previous !== undefined && upper !== undefined && upper !== ']') {
if (upper === '\\') {
const step = escapeAt(source, cursor + 1);
if (isRefusal(step)) return step;
if (!isAscii(previous)) return refuse(`${previous}-\\`, cursor - 1, RANGE_WHY, undefined);
previous = undefined;
cursor = step;
continue;
}
if (!isAscii(previous) || !isAscii(upper)) {
return refuse(`${previous}-${upper}`, cursor - 1, RANGE_WHY, undefined);
}
previous = undefined;
cursor += 2;
continue;
}
previous = char;
cursor += 1;
}
return refuse('[', at, 'opens a bracket expression that is never closed', undefined);
}
/**
* The first construct in `source` the two engines read differently, or `undefined` when the whole
* pattern is in the subset they agree on.
*
* Only the POSTGRES direction is judged: `matches` is handed a built `RegExp`, so JavaScript has
* already accepted the source by the time this runs.
*/
export function unportableConstruct(source: string): UnportablePattern | undefined {
let at = 0;
/** Whether a `{n}` here would be a quantifier at all — it needs something to repeat. */
let repeatable = false;
while (at < source.length) {
const char = source[at] ?? '';
if (char === '\u0000') return { ...NUL, at };
let step: Step = at + 1;
if (char === '\\') step = escapeAt(source, at);
else if (char === '[') step = bracketAt(source, at);
else if (char === '(') step = groupAt(source, at);
else if (char === '{') {
// `/^{2}$/` compiles in JavaScript under Annex B and is `invalid regular expression` on the
// server, so the migration is the thing that fails. Refusing here moves it to the line that
// wrote it — and a `{` with nothing before it is that same error, which is why the quantifier
// has to be judged in position and not by its own shape.
const quantifier = repeatable ? QUANTIFIER.exec(source.slice(at)) : null;
step =
quantifier === null
? refuse('{', at, 'opens no quantifier Postgres can read', '\\{')
: at + quantifier[0].length;
} else if (char === '.') {
// `'a\nb' ~ 'a.b'` is TRUE and `/a.b/.test('a\nb')` is false: ARE's `.` matches a newline.
step = refuse('.', at, 'matches a newline in Postgres and never in JavaScript', '[^\\n\\r]');
}
if (isRefusal(step)) return step;
// `^`, `$`, `|` and an opening `(` leave nothing to repeat; everything else does, including a
// `)` that closed a group and a `]` that closed a class.
repeatable = char !== '^' && char !== '$' && char !== '|' && char !== '(';
at = step;
}
return undefined;
}
+5
-5
{
"name": "@ultimat3/entity",
"version": "15.0.0",
"version": "16.0.0",
"description": "A table + its domain type + invariants the database also enforces",

@@ -34,7 +34,7 @@ "license": "MIT",

"dependencies": {
"@ultimat3/core": "15.0.0",
"@ultimat3/db": "15.0.0",
"@ultimat3/schema": "15.0.0",
"@ultimat3/time": "15.0.0"
"@ultimat3/core": "16.0.0",
"@ultimat3/db": "16.0.0",
"@ultimat3/schema": "16.0.0",
"@ultimat3/time": "16.0.0"
}
}

@@ -158,2 +158,33 @@ # @ultimat3/entity 🗄️

A `RegExp` is not a predicate and does reach the database: `c.slug.matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)`
is `kind: 'check'` and emits `slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'`. The **same string** runs in both
places — nothing is translated — so a construct the two engines read differently is REFUSED where it
is written, with the portable spelling in the `fix` where one exists (`\w` -> `[A-Za-z0-9_]`, `.` ->
`[^\n\r]`) and the app-only predicate where none does (`\b` is a word boundary to `.test()` and a
BACKSPACE to Postgres). `i` becomes `~*`; every other flag is refused.
`isNull()` / `isNotNull()` are the only pair in the vocabulary TOTAL over NULL — `IS NULL` answers
true or false for every input, and the app side reads an ABSENT key and a stored `null` as one value.
`iff(a, b)` is the biconditional over two predicates, rendered `(a) = (b)`, which is what Postgres
spells one as:
```ts
import { entity, enumerated, iff, invariant, timestamp, uuid } from '@ultimat3/entity';
export const publishable = entity('publishable', {
columns: {
id: uuid().primaryKey(),
status: enumerated(['draft', 'published']).default('draft'),
publishedAt: timestamp().nullable(),
},
invariants: (c) => [
invariant('post_publish_coherent', iff(c.status.eq('published'), c.publishedAt.isNotNull())),
// check ((status = 'published') = (published_at is not null))
],
});
```
A `satisfies(fn, [...])` that could be written this way should be: only one of the two reaches the
database, and `x verify` reports the other as a rule the database does not know.
## One typed handle

@@ -160,0 +191,0 @@

@@ -5,2 +5,3 @@ // Two things every column builder needs and neither owns: how a rejected value is DESCRIBED, and

import { literal } from '@ultimat3/db';
import { describeValue } from '@ultimat3/schema';

@@ -25,7 +26,14 @@

const quote = (value: string): string => `'${value.replaceAll("'", "''")}'`;
/**
* The value list of a closed set, quoted by `@ultimat3/db`'s `literal()` — the framework's one
* splice rule, imported downward rather than restated. This file carried its own
* `'${v.replaceAll("'", "''")}'` and `expr.ts` carried the same line; two copies of an escape is two
* places a hardening has to land, and only one of them ever does. The members are an APP's own
* `enumerated([...])` array, so they reach `create table` as text nothing validated.
*
* `.text` because a column's `check` is a bare string all the way to the DDL, not a `SqlFragment`.
*/
export const oneOf =
(values: readonly string[]) =>
(name: string): string =>
`${name} in (${values.map(quote).join(', ')})`;
`${name} in (${values.map((value) => literal(value).text).join(', ')})`;

@@ -10,4 +10,3 @@ // Single responsibility: what `@>`, `<@`, `&&` and a JSON key test MEAN, written once so the

/** Absent and NULL are one thing, exactly as they are to every other predicate. */
const isNull = (value: unknown): boolean => value === null || value === undefined;
import { isNullish as isNull } from './is-null';

@@ -14,0 +13,0 @@ const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>

@@ -10,7 +10,27 @@ // The invariant expression language. One declaration compiles to two enforcement points: a

// does not know this rule — silently pretending it reached Postgres would be worse.
//
// A `RegExp` is the other case and it DOES reach SQL, because nothing about it is translated: the
// source `pattern.test` runs is the source spliced into the CHECK. `pattern-portability.ts` is what
// makes that legal, and `@ultimat3/db`'s `literal()` is what keeps the splice inside its own quotes.
import { literal as sqlLiteral } from '@ultimat3/db';
import { invariantViolated } from './errors';
import { isNullish } from './is-null';
import { unportableConstruct } from './pattern-portability';
import { refuseInvariant } from './refuse';
import type { ColumnMap } from './types';
/**
* A declared operand as SQL TEXT. The escape itself is `@ultimat3/db`'s `literal()` — tier 1 owns
* that rule and this is an ordinary downward import — and nothing here re-spells it; the wrapper
* exists for two narrower reasons.
*
* It takes the four types a CHECK operand can be, rather than `unknown`: every call below already
* knows which one it holds, and a widened parameter would put `String(someObject)` into statement
* text as `[object Object]`. And it unwraps `.text`, because `Invariant.sql` is a bare string that
* `@ultimat3/db` re-renders at DDL time — a `SqlFragment` cannot survive that round trip.
*/
const literal = (value: string | number | boolean | bigint): string =>
typeof value === 'string' ? sqlLiteral(value).text : String(value);
export type Row = Readonly<Record<string, unknown>>;

@@ -42,3 +62,7 @@

contains(value: string): Expr;
/** A `RegExp` reaches the database; a function is app-only. */
/**
* A `RegExp` reaches the database as `~`/`~*` over its own source; a function is app-only. A
* construct the two engines read differently is refused at declaration, never emitted as a
* lookalike — `pattern-portability.ts` names the subset and why each exclusion is in it.
*/
matches(pattern: RegExp | ((value: string) => boolean)): Expr;

@@ -48,2 +72,11 @@ atLeast(value: number | bigint): Expr;

isTrue(): Expr;
/**
* `col is null` / `col is not null`, and the only pair in this vocabulary that is TOTAL over
* NULL in both halves: Postgres' `IS NULL` answers true or false for every input including NULL,
* and the app side reads absent and `null` as one value (`is-null.ts`). Every other operator
* here answers NULL in SQL for a NULL operand, and a CHECK PASSES on NULL — which is why these
* two are what an `iff` can be built out of.
*/
isNull(): Expr;
isNotNull(): Expr;
/** Money is two physical columns; these are how a rule names one of them. */

@@ -81,5 +114,2 @@ readonly minor: ColumnExpr;

const literal = (value: unknown): string =>
typeof value === 'string' ? `'${value.replaceAll("'", "''")}'` : String(value);
/**

@@ -113,2 +143,37 @@ * The Postgres operator a `RegExp`'s flags mean — the second half of "one declaration, two

/**
* The SQL a `RegExp` becomes — or the refusal naming the construct that would have made the two
* halves mean different things.
*
* Nothing is TRANSLATED here and nothing ever should be: the string handed to `pattern.test` and
* the string spliced into the CHECK are the SAME string, and `unportableConstruct` is what makes
* that legal. Emitting a "close enough" POSIX rewrite of a JavaScript-only construct would ship two
* rules under one name, which is strictly worse than the `assert` a predicate already gives you.
*
* Flags are judged first: a flag is a property of the whole pattern and a construct is one position
* inside it, so the refusal an author can act on without reading an index goes out first.
*
* The source is spliced through `literal`, never quoted here, and a PATTERN is the sharpest case
* for why that rule is `@ultimat3/db`'s and not a doubled quote: measured on 18.4, `'dd' ~ '^\d+$'`
* is FALSE with `standard_conforming_strings` on and **TRUE** with it off, because the server
* compiles `^d+$` — a CHECK enforcing a pattern the author never wrote, with no error anywhere.
* A backslash is in almost every real pattern, so almost every real pattern depends on the `E'…'`
* half. `pg-invariant-pattern.live.test.ts` runs that exact pair under both settings.
*/
const patternSql = (pattern: RegExp): string => {
const operator = matchOperator(pattern);
const unportable = unportableConstruct(pattern.source);
const spelled = `/${pattern.source}/${pattern.flags}`;
if (unportable !== undefined) {
return refuseInvariant(
'matches',
`${spelled} uses ${unportable.construct} at index ${unportable.at}, which ${unportable.why} — the CHECK and pattern.test() would answer differently for the same row`,
unportable.instead === undefined
? `matches((value) => ${spelled}.test(value)) # app-only: the rule stays in TS and reports sql: null, so no CHECK claims to enforce it`
: `write ${unportable.instead} where ${spelled} has ${unportable.construct} — one meaning in both engines — then x db gen`,
);
}
return `${operator} ${literal(pattern.source)}`;
};
const check = (

@@ -166,6 +231,3 @@ paths: readonly (readonly string[])[],

// rather than during migration generation, where the entity name is all anyone would see.
const emitted =
pattern instanceof RegExp
? `${matchOperator(pattern)} ${literal(pattern.source)}`
: undefined;
const emitted = pattern instanceof RegExp ? patternSql(pattern) : undefined;
return one(

@@ -203,2 +265,12 @@ `${term.label} must match ${pattern instanceof RegExp ? pattern.source : pattern.name || 'the rule'}`,

isNull: () =>
one(`${term.label} is not set`, (resolve) => `${term.sql(resolve)} is null`, isNullish),
isNotNull: () =>
one(
`${term.label} is set`,
(resolve) => `${term.sql(resolve)} is not null`,
(value) => !isNullish(value),
),
get minor() {

@@ -272,2 +344,67 @@ return part(term, 'minor');

/**
* `a` and `b` hold together or not at all — the biconditional, rendered `(a) = (b)`, which is what
* Postgres spells one as: `=` between two booleans IS iff there.
*
* A FUNCTION and not a method on `Expr`, for two reasons that both come from the type. `Expr` is
* exported, so a required member is a breaking change to anything implementing it structurally; and
* `kind: 'unique'` is an `Expr` whose `toSql` is a COLUMN LIST, so `c.unique([…]).iff(…)` would be a
* method that exists on the type and is meaningless for some of its values. Refusing that operand in
* one place beats putting the method where it cannot mean anything. Symmetric reads symmetric, too.
*
* **`=` and not `is not distinct from`, decided on a measurement.** With both operands total the two
* are identical for all four boolean pairs. They part when an operand is NULL — a predicate on a
* nullable column — and there `=` answers NULL, which a CHECK PASSES, while `is not distinct from`
* answers false, which a CHECK REFUSES. The app side reads a NULL operand as false either way, so
* the total form is the one that refuses a row TypeScript ACCEPTED: `(NULL) is not distinct from
* (false)` is false where `false === false` is true. That is the raw `23514` in place of
* `X_INVARIANT_VIOLATED` this whole file exists against, and it is why the more permissive spelling
* is the safer one. `pg-invariant-null.live.test.ts` measures both.
*
* So an operand that can be NULL leaves the CHECK permissive — the language's one existing
* disagreement, inherited here and not widened. `isNull()`/`isNotNull()` are total, which is what
* makes a rule built from them exact.
*/
export const iff = (left: Expr, right: Expr): Expr => {
for (const side of [left, right] as const) {
if (side.kind !== 'unique') continue;
// The columns it names, so the pasted line is the rule the author already meant to declare —
// never a `<name>` for them to fill in, which is the placeholder `refuse.test.ts` refuses.
//
// `JSON.stringify` and never `'${column}'`: a column path is a VALUE reaching TypeScript
// SOURCE, which is this file's own hazard one layer up. `columns: { "o'brien": text() }` is a
// legal declaration and `unique()` is reached untyped besides, so a quote ends the literal and
// the fix stops parsing; a backslash is the half doubling the quote would still have missed.
// `errors.ts`'s `asLiteral` is the same rule for the same reason.
const columns = side.paths.map((path) => path.join('.'));
const list = columns.map((column) => JSON.stringify(column)).join(', ');
const name = JSON.stringify(`${columns.join('_')}_unique`);
refuseInvariant(
'iff',
`${side.message} is a unique constraint, whose SQL is a column list and not a predicate`,
`invariant(${name}, c.unique([${list}])) # uniqueness is its own invariant; iff takes two predicates, e.g. iff(c.status.eq('published'), c.publishedAt.isNotNull())`,
);
}
const seen = new Set<string>();
const paths: (readonly string[])[] = [];
for (const path of [...left.paths, ...right.paths]) {
const key = path.join('.');
if (seen.has(key)) continue;
seen.add(key);
paths.push(path);
}
return check(
paths,
`${left.message} exactly when ${right.message}`,
(resolve) => {
// One app-only operand makes the WHOLE rule app-only: `(null) = (…)` is not a predicate, and
// emitting half of a biconditional would enforce something the author never wrote.
const a = left.toSql(resolve);
const b = right.toSql(resolve);
return a === null || b === null ? null : `(${a}) = (${b})`;
},
(row) => left.holds(row) === right.holds(row),
);
};
/**
* The `c` an invariant is written against. Still a Proxy even though `InvariantColumns<C>` now

@@ -274,0 +411,0 @@ * catches a typo at compile time: a JS caller, a dynamically built rule and a `satisfies()` column

@@ -68,2 +68,3 @@ // The public surface of @ultimat3/entity. Explicit, never `export *`.

export type { ColumnExpr, Expr, InvariantColumns, Resolve } from './expr';
export { iff } from './expr';
/** The two DECLARED capabilities' refusals — a third-party driver raises the same ones. */

@@ -70,0 +71,0 @@ export type { IllegalTransition } from './feature-errors';

@@ -15,2 +15,3 @@ // Single responsibility: what a `Predicate` MEANS in the in-memory driver — equality, ordering and

import { instantMicros } from './instant';
import { isNullish as isNull } from './is-null';
import type { Predicate } from './tenancy';

@@ -38,5 +39,2 @@ import type { ColumnKind } from './types';

/** Absent and NULL are one thing to a predicate: a column the projection left out is not a value. */
const isNull = (value: unknown): boolean => value === null || value === undefined;
const sign = <T extends number | bigint | string>(left: T, right: T): number =>

@@ -43,0 +41,0 @@ left < right ? -1 : left > right ? 1 : 0;

Sorry, the diff of this file is too big to display