diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fcaa5c..e6a8794 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.3 + +- A user column named after a system column (`tableoid`, `xmin`, `cmin`, `xmax`, `cmax`, `ctid`) is refused in CREATE TABLE, ADD COLUMN and RENAME COLUMN, as postgres does: `column name "xmin" conflicts with a system column name`. The migration validator accepted DDL postgres rejects. + ## 4.0.2 Gaps found by running 80 production projects' migrations and seeds through the agent's validator on pg-mem and on PGlite; all 80 now match. Requires `@tinbase/pgsql-ast-parser` 12.2.1. diff --git a/package.json b/package.json index ad12880..dececcf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tinbase/pg-mem", - "version": "4.0.2", + "version": "4.0.3", "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/column.ts b/src/column.ts index 1b0a62a..57871d8 100644 --- a/src/column.ts +++ b/src/column.ts @@ -1,5 +1,6 @@ import { _Column, IValue, _IIndex, NotSupported, _Transaction, QueryError, _IType, SchemaField, ChangeHandler, nil, ISubscription, DropHandler } from './interfaces-private'; import type { MemoryTable } from './table'; +import { assertNotSystemColumn } from './table'; import { Evaluator } from './evaluator'; import { ColumnConstraint, AlterColumn, Expr, toSql, DataTypeDef } from 'pgsql-ast-parser'; import { policiesDependingOn } from './execution/policy-deps'; @@ -133,6 +134,7 @@ export class ColRef implements _Column { if (this.table.getColumnRef(to, true)) { throw new QueryError(`Column "${to}" already exists`); } + assertNotSystemColumn(to); // first, move data (this cannot throw => OK to modify mutable data) this.table.remapData(t, v => { diff --git a/src/table.ts b/src/table.ts index fadc3ca..38874b4 100644 --- a/src/table.ts +++ b/src/table.ts @@ -77,6 +77,15 @@ class ColumnManager { } } +/** every postgres table has these hidden columns; no user column may take their names */ +const SYSTEM_COLUMNS = new Set(['tableoid', 'xmin', 'cmin', 'xmax', 'cmax', 'ctid']); + +export function assertNotSystemColumn(name: string) { + if (SYSTEM_COLUMNS.has(name)) { + throw new QueryError(`column name "${name}" conflicts with a system column name`, '42701'); + } +} + export class MemoryTable extends DataSourceBase implements IMemoryTable, _ITable { comment: string | nil; readonly rls: TableRls = emptyRls(); @@ -264,6 +273,7 @@ export class MemoryTable extends DataSourceBase implements IMemoryTable, _I if (this.columnMgr.has(column.name)) { throw new QueryError(`Column "${column.name}" already exists`); } + assertNotSystemColumn(column.name); const type = typeof column.type === 'string' ? this.ownerSchema.getType(column.type) : column.type; diff --git a/src/tests/corpus-parity.spec.ts b/src/tests/corpus-parity.spec.ts index 2c49158..8bb74f1 100644 --- a/src/tests/corpus-parity.spec.ts +++ b/src/tests/corpus-parity.spec.ts @@ -668,6 +668,16 @@ describe('corpus parity', () => { }); }); + describe('system column names', () => { + it('are refused for user columns, as in postgres', () => { + expectQueryError(() => none(`create table t ("xmin" text)`), /column name "xmin" conflicts with a system column name/); + none(`create table z (id int)`); + expectQueryError(() => none(`alter table z add column ctid text`), /conflicts with a system column name/); + expectQueryError(() => none(`alter table z rename column id to tableoid`), /conflicts with a system column name/); + none(`create table w (xminimum int, "Xmin" int)`); + }); + }); + describe('CREATE OR REPLACE TRIGGER', () => { it('replaces an existing trigger', () => { none(`create table o (id int, n int);