From 4114a27bff1f8bb2c8f33e035c92af28b63a03cc Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 3 Apr 2026 12:44:01 +0200 Subject: [PATCH] fix: use ad.adrelid instead of pg_attrdef OID in pg_get_expr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: SQLSTATE[XX000]: Internal error: 7 ERROR: invalid attnum 9 for relation "pg_attrdef" pg_get_expr requires the OID of the relation that owns the expression as its second argument. Using 'pg_catalog.pg_attrdef'::regclass passes the OID of the catalog table itself, which causes "invalid attnum" errors when tables have GENERATED ALWAYS AS stored columns. The correct relation OID is ad.adrelid (the table the default/generated expression belongs to). The autoincrement detection on the same line already uses ad.adrelid correctly — this aligns the other two calls. --- .github/workflows/tests.yml | 14 +++++++++++++ src/Database/Drivers/PgSqlDriver.php | 4 ++-- tests/Database/Reflection.postgre.phpt | 29 ++++++++++++++++++++++++++ tests/databases.github.ini | 6 ++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 875732cde..37c8b1aaa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,6 +72,20 @@ jobs: --health-timeout 5s --health-retries 5 + postgres15: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: nette_test + ports: + - 5434:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + mssql: image: mcr.microsoft.com/mssql/server:latest env: diff --git a/src/Database/Drivers/PgSqlDriver.php b/src/Database/Drivers/PgSqlDriver.php index deb3e927e..ec23f84e1 100644 --- a/src/Database/Drivers/PgSqlDriver.php +++ b/src/Database/Drivers/PgSqlDriver.php @@ -148,11 +148,11 @@ public function getColumns(string $table): array ELSE NULL END AS size, NOT (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS nullable, - pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass)::varchar AS default, + pg_catalog.pg_get_expr(ad.adbin, ad.adrelid)::varchar AS default, coalesce(co.contype = 'p' AND (seq.relname IS NOT NULL OR strpos(pg_catalog.pg_get_expr(ad.adbin, ad.adrelid), 'nextval') = 1), FALSE) AS autoincrement, coalesce(co.contype = 'p', FALSE) AS primary, coalesce(col_description(c.oid, a.attnum)::varchar, '') AS comment, - coalesce(seq.relname, substring(pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass) from 'nextval[(]''"?([^''"]+)')) AS sequence + coalesce(seq.relname, substring(pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) from 'nextval[(]''"?([^''"]+)')) AS sequence FROM pg_catalog.pg_attribute AS a JOIN pg_catalog.pg_class AS c ON a.attrelid = c.oid diff --git a/tests/Database/Reflection.postgre.phpt b/tests/Database/Reflection.postgre.phpt index b3788e82a..cbdde4f6b 100644 --- a/tests/Database/Reflection.postgre.phpt +++ b/tests/Database/Reflection.postgre.phpt @@ -76,3 +76,32 @@ test('Tables in schema', function () use ($connection) { $connection->query('SET search_path TO one, two'); Assert::same(['one_slave_fk', 'one_two_fk'], names($driver->getForeignKeys('slave'))); }); + + +test('Table with GENERATED ALWAYS AS stored columns', function () use ($connection) { + $ver = $connection->query('SHOW server_version')->fetchField(); + if (version_compare($ver, '12') < 0) { + Tester\Environment::skip("GENERATED ALWAYS AS requires PostgreSQL 12+, running $ver."); + } + + Nette\Database\Helpers::loadFromFile($connection, Tester\FileMock::create(' + DROP TABLE IF EXISTS "generated_test"; + + CREATE TABLE "generated_test" ( + "id" serial PRIMARY KEY, + "first_name" varchar(50) NOT NULL, + "last_name" varchar(50) NOT NULL, + "full_name" text GENERATED ALWAYS AS ("first_name" || \' \' || "last_name") STORED + ); + ')); + + $driver = $connection->getDriver(); + $columns = $driver->getColumns('generated_test'); + $columnNames = array_column($columns, 'name'); + + Assert::same(['id', 'first_name', 'last_name', 'full_name'], $columnNames); + + $fullNameCol = $columns[3]; + Assert::same('full_name', $fullNameCol['name']); + Assert::same('TEXT', $fullNameCol['nativetype']); +}); diff --git a/tests/databases.github.ini b/tests/databases.github.ini index d00f44271..ba5deb2c8 100644 --- a/tests/databases.github.ini +++ b/tests/databases.github.ini @@ -24,6 +24,12 @@ username = postgres password = postgres options[newDateTime] = yes +[postgresql 15] +dsn = "pgsql:host=127.0.0.1;port=5434;dbname=nette_test" +username = postgres +password = postgres +options[newDateTime] = yes + [sqlsrv] dsn = "sqlsrv:Server=localhost,1433;Database=nette_test" username = SA