From f0535fdc84ce2b64291bfe9ca6e92a57715f93bb Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 10 Oct 2026 16:45:00 +0300 Subject: [PATCH] fix(errors): a missing column is not reported as a missing table (#1310) PostgreSQL words a missing INSERT/UPDATE column as column "x" of relation "t" does not exist; the relation pattern matched it and the user read Table "t" does not exist for a table that exists. Missing columns (PostgreSQL, MySQL, SQLite) are their own error, checked before the table patterns. --- lib/core/database/database_error_mapper.dart | 33 ++++++++++++++++ .../database/querya_database_exception.dart | 16 ++++++++ .../database/database_error_mapper_test.dart | 39 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/lib/core/database/database_error_mapper.dart b/lib/core/database/database_error_mapper.dart index a140e7a3..117c71a5 100644 --- a/lib/core/database/database_error_mapper.dart +++ b/lib/core/database/database_error_mapper.dart @@ -87,6 +87,21 @@ QueryaDatabaseException mapDatabaseError( originalError: error, stackTrace: stackTrace, ); + } else if (_columnMatch(raw) case final column?) { + // Before the table patterns: `column "x" of relation "t" does not exist` + // contains `relation "t" does not exist` and is not a missing table. + mapped = ColumnNotFoundException( + column.table == null + ? 'Column "${column.name}" does not exist' + : 'Column "${column.name}" does not exist in "${column.table}"', + columnName: column.name, + tableName: column.table, + remediationHint: + 'Check the column name, or refresh the object tree if it was added ' + 'or renamed', + originalError: error, + stackTrace: stackTrace, + ); } else if (_firstMatch(raw, [ RegExp(r'relation "([^"]+)" does not exist', caseSensitive: false), RegExp(r"table '([^']+)' doesn't exist", caseSensitive: false), @@ -195,6 +210,24 @@ String _innermost(String raw) { bool _hasAny(String lower, List needles) => needles.any(lower.contains); +/// A missing column in the words of PostgreSQL, MySQL or SQLite. +({String name, String? table})? _columnMatch(String raw) { + final pg = RegExp( + r'column "([^"]+)" of relation "([^"]+)" does not exist', + caseSensitive: false, + ).firstMatch(raw); + if (pg != null) return (name: pg.group(1)!, table: pg.group(2)); + for (final p in [ + RegExp(r'column "([^"]+)" does not exist', caseSensitive: false), + RegExp(r"unknown column '([^']+)'", caseSensitive: false), + RegExp(r'no such column: ([\w."]+)', caseSensitive: false), + ]) { + final m = p.firstMatch(raw); + if (m != null) return (name: m.group(1)!, table: null); + } + return null; +} + String? _firstMatch(String text, List patterns) { for (final p in patterns) { final m = p.firstMatch(text); diff --git a/lib/core/database/querya_database_exception.dart b/lib/core/database/querya_database_exception.dart index b5a678e8..5b663fc9 100644 --- a/lib/core/database/querya_database_exception.dart +++ b/lib/core/database/querya_database_exception.dart @@ -108,6 +108,22 @@ final class TableNotFoundException extends QueryaDatabaseException { final String? tableName; } +/// A column referenced by a statement does not exist (#1310). +final class ColumnNotFoundException extends QueryaDatabaseException { + const ColumnNotFoundException( + super.message, { + this.columnName, + this.tableName, + super.detailedExplanation, + super.remediationHint, + super.originalError, + super.stackTrace, + }); + + final String? columnName; + final String? tableName; +} + /// A lock could not be acquired in time (busy / locked / deadlock). final class LockTimeoutException extends QueryaDatabaseException { const LockTimeoutException( diff --git a/test/core/database/database_error_mapper_test.dart b/test/core/database/database_error_mapper_test.dart index 1c510558..e3541968 100644 --- a/test/core/database/database_error_mapper_test.dart +++ b/test/core/database/database_error_mapper_test.dart @@ -10,6 +10,45 @@ import 'package:querya_desktop/core/database/redis_connection.dart'; import 'package:querya_desktop/core/database/sqlite_connection.dart'; void main() { + group('missing columns (#1310)', () { + test('a column of an existing table is not a missing table', () { + final e = mapDatabaseError( + Exception('ERROR: column "nickname" of relation "users" does not exist'), + driver: DatabaseDriver.postgres, + ); + expect(e, isA()); + expect((e as ColumnNotFoundException).columnName, 'nickname'); + expect(e.tableName, 'users'); + expect(e.message, 'Column "nickname" does not exist in "users"'); + expect(e, isNot(isA())); + }); + + test('PostgreSQL, MySQL and SQLite wordings without a table', () { + for (final (text, driver, name) in [ + ('column "price" does not exist', DatabaseDriver.postgres, 'price'), + ( + "Unknown column 'price' in 'field list'", + DatabaseDriver.mysql, + 'price' + ), + ('no such column: orders.price', DatabaseDriver.sqlite, 'orders.price'), + ]) { + final e = mapDatabaseError(Exception(text), driver: driver); + expect(e, isA(), reason: text); + expect((e as ColumnNotFoundException).columnName, name); + } + }); + + test('a missing relation is still a missing table', () { + final e = mapDatabaseError( + Exception('ERROR: relation "orders" does not exist'), + driver: DatabaseDriver.postgres, + ); + expect(e, isA()); + expect((e as TableNotFoundException).tableName, 'orders'); + }); + }); + group('mapDatabaseError PostgreSQL', () { test('wrong password is AuthFailedException', () { final e = mapDatabaseError(