diff --git a/CHANGELOG.md b/CHANGELOG.md index a003fcd..7c9bd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable changes to `@tinbase/pg-mem`, the tinbase fork of pg-mem. Released from `main`, which carries the scoped package name. Upstream is tracked through the `upstream` remote (`oguimbal/pg-mem`) rather than a branch; the leftover `master` is vestigial. +## 4.0.1 + +- Comparisons across type categories fail with Postgres' own error, `operator does not exist: uuid = text` (was `cannot cast type text to uuid`), types spelled as Postgres spells them. Also for `x IN (select col ...)`. The SQL that is rejected does not change. + ## 4.0.0 Postgres parity for validating real Supabase migrations: pg-mem now rejects what Postgres rejects, rolls back schema changes, and enforces row-level security on every read and write path. Found by running 250 real RapidNative projects' migrations and seeds through pg-mem and PGlite side by side (`tools/corpus-diff`); all 5 sets of 50 now match. Requires `@tinbase/pgsql-ast-parser` 12.2.0. diff --git a/package.json b/package.json index 3d66eb4..16ed6af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tinbase/pg-mem", - "version": "4.0.0", + "version": "4.0.1", "description": "Fork of pg-mem with extended Postgres conformance (PL/pgSQL, triggers, RLS, correlated subqueries, MERGE, ranges, full-text, partitioning, ...). Tracks oguimbal/pg-mem; pending upstream PR #476.", "main": "index.js", "publishConfig": { diff --git a/src/parser/expression-builder.ts b/src/parser/expression-builder.ts index a7c6476..f268c3c 100644 --- a/src/parser/expression-builder.ts +++ b/src/parser/expression-builder.ts @@ -394,7 +394,7 @@ function buildIn(left: Expr, array: Expr, inclusive: boolean): IValue { const col = (rightValue.type as ArrayType).of; const leftUnknown = leftValue.isConstantLiteral && leftValue.type.primary === DataType.text; if (col && !leftUnknown && crossesTypeCategory(leftValue.type, col)) { - throw new QueryError(`operator does not exist: ${leftValue.type.name} = ${col.name}`, '42883'); + throw new QueryError(`operator does not exist: ${pgTypeName(leftValue.type)} = ${pgTypeName(col)}`, '42883'); } } if (array.type !== 'list' && rightValue.type.primary !== DataType.list && rightValue.type.primary !== DataType.array) { @@ -405,6 +405,19 @@ function buildIn(left: Expr, array: Expr, inclusive: boolean): IValue { } +/** a type as postgres spells it in error messages (format_type) */ +function pgTypeName(t: _IType): string { + switch (t.primary) { + case DataType.bool: return 'boolean'; + case DataType.float: return 'double precision'; + case DataType.decimal: return 'numeric'; + case DataType.timestamptz: return 'timestamp with time zone'; + case DataType.timestamp: return 'timestamp without time zone'; + case DataType.time: return 'time without time zone'; + } + return t.name; +} + function buildBinary(val: ExprBinary): IValue { let leftValue = _buildValue(val.left); let rightValue = _buildValue(val.right); @@ -437,6 +450,13 @@ export function buildBinaryValue(leftValue: IValue, op: BinaryOperator, rightVal rightValue = doMap(rightValue); } } + // two typed operands from different type categories: postgres has no such operator, and + // says so ("operator does not exist: uuid = text") - which is also what tells the author + // which side to cast + const unknown = (v: IValue) => v.isConstantLiteral && v.type.primary === DataType.text; + if (!unknown(leftValue) && !unknown(rightValue) && crossesTypeCategory(leftValue.type, rightValue.type)) { + throw new QueryError(`operator does not exist: ${pgTypeName(leftValue.type)} ${op} ${pgTypeName(rightValue.type)}`, '42883'); + } const type: _IType = reconciliateTypes([leftValue, rightValue]); leftValue = leftValue.cast(type); rightValue = rightValue.cast(type); diff --git a/src/tests/corpus-parity.spec.ts b/src/tests/corpus-parity.spec.ts index 3068b01..b24dcb5 100644 --- a/src/tests/corpus-parity.spec.ts +++ b/src/tests/corpus-parity.spec.ts @@ -124,6 +124,13 @@ describe('corpus parity', () => { } }); + it('says which operator does not exist, as postgres does', () => { + expectQueryError(() => many(`select 1 from t where u = s`), /operator does not exist: uuid = text/); + expectQueryError(() => many(`select 1 from t where b = s`), /operator does not exist: boolean = text/); + expectQueryError(() => many(`select 1 from t where s = i`), /operator does not exist: text = integer/); + expectQueryError(() => many(`select 1 from t where u in (select s from t)`), /operator does not exist: uuid = text/); + }); + it('still coerces untyped literals and widens within a category', () => { for (const e of [`u = '00000000-0000-0000-0000-000000000001'`, `i = '1'`, 'i = bi', 'bi = i', 'i = n', 'd = ts', 'ts > d', 'd = now()', 's = v', 'u::text = s', `s || i = 'x1'`, `j ? s`, `concat(u, 'x') = s`]) {