From 69e0a1bb9e2198fb43ef9d6d97641387424e269c Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 10 Mar 2026 18:19:53 +0000 Subject: [PATCH 1/3] Add pqerror package Add a pqerror package with a list of error codes. Typical usage might be something like: pqErr, ok := errors.AsType[*pq.Error](err) if ok && pqErr == pqerror.UniqueViolation { return fmt.Errorf("user already exists") } To make this a bit more convenient, it also adds a pqerror.As() function: pqErr := pqerror.As(err, pqerror.UniqueViolation) if pqErr != nil { log.Fatalf("email %q already exsts", email) } if err != nil { return err } This also moves most of the error stuff to the pqerror package, with type aliases in the pq package so it won't break anything. Has to export Error.Query to make this work, which is okay. Keep most of the tests in the main pq package for now, to ensure this works. Fixes #492 --- CHANGELOG.md | 21 ++ deprecated.go | 43 --- error.go | 535 +----------------------------------- error_test.go | 2 +- pqerror/as.go | 25 ++ pqerror/as_126.go | 21 ++ pqerror/codes.go | 581 ++++++++++++++++++++++++++++++++++++++++ pqerror/deprecated.go | 44 +++ pqerror/example_test.go | 25 ++ pqerror/gen.go | 146 ++++++++++ pqerror/pqerror.go | 247 +++++++++++++++++ pqerror/pqerror_test.go | 61 +++++ 12 files changed, 1186 insertions(+), 565 deletions(-) create mode 100644 pqerror/as.go create mode 100644 pqerror/as_126.go create mode 100644 pqerror/codes.go create mode 100644 pqerror/deprecated.go create mode 100644 pqerror/example_test.go create mode 100644 pqerror/gen.go create mode 100644 pqerror/pqerror.go create mode 100644 pqerror/pqerror_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 112e8c4f6..95a3949fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,22 @@ unreleased - Support `sslrootcert=system` and use `~/.postgresql/root.crt` as the default value of sslrootcert ([#1280], [#1281]). +- Add a new `pqerror` package with PostgreSQL error codes ([#1275]). + + For example, to test if an error is a UNIQUE constraint violation: + + pqErr, ok := pq.AsType[*pq.Error](err) + if ok && pqErr.Code == pqerror.UniqueViolation { + log.Fatalf("email %q already exsts", email) + } + + To make this a bit more convenient, it also adds a pqerror.As() function: + + pqErr := pqerror.As(err, pqerror.UniqueViolation) + if pqErr != nil { + log.Fatalf("email %q already exsts", email) + } + ### Fixes - Fix SSL key permission check to allow modes stricter than 0600/0640#1265 ([#1265]). @@ -48,6 +64,7 @@ unreleased [#1270]: https://github.com/lib/pq/pull/1270 [#1271]: https://github.com/lib/pq/pull/1271 [#1272]: https://github.com/lib/pq/pull/1272 +[#1275]: https://github.com/lib/pq/pull/1275 [#1277]: https://github.com/lib/pq/pull/1277 [#1278]: https://github.com/lib/pq/pull/1278 [#1279]: https://github.com/lib/pq/pull/1279 @@ -57,6 +74,7 @@ unreleased [#1283]: https://github.com/lib/pq/pull/1283 [#1285]: https://github.com/lib/pq/pull/1285 + v1.11.2 (2026-02-10) -------------------- This fixes two regressions: @@ -167,6 +185,8 @@ newer. Previously PostgreSQL 8.4 and newer were supported. - Handle ErrorResponse in readReadyForQuery and return proper error ([#1136]). +- Detect COPY even if the query starts with whitespace or comments ([#1198]). + - CopyIn() and CopyInSchema() now work if the list of columns is empty, in which case it will copy all columns ([#1239]). @@ -192,6 +212,7 @@ newer. Previously PostgreSQL 8.4 and newer were supported. [#1180]: https://github.com/lib/pq/pull/1180 [#1184]: https://github.com/lib/pq/pull/1184 [#1188]: https://github.com/lib/pq/pull/1188 +[#1198]: https://github.com/lib/pq/pull/1198 [#1211]: https://github.com/lib/pq/pull/1211 [#1212]: https://github.com/lib/pq/pull/1212 [#1214]: https://github.com/lib/pq/pull/1214 diff --git a/deprecated.go b/deprecated.go index 795d3328c..8ab257cba 100644 --- a/deprecated.go +++ b/deprecated.go @@ -14,49 +14,6 @@ type PGError interface { Get(k byte) (v string) } -// Get implements the legacy PGError interface. -// -// Deprecated: new code should use the fields of the Error struct directly. -func (e *Error) Get(k byte) (v string) { - switch k { - case 'S': - return e.Severity - case 'C': - return string(e.Code) - case 'M': - return e.Message - case 'D': - return e.Detail - case 'H': - return e.Hint - case 'P': - return e.Position - case 'p': - return e.InternalPosition - case 'q': - return e.InternalQuery - case 'W': - return e.Where - case 's': - return e.Schema - case 't': - return e.Table - case 'c': - return e.Column - case 'd': - return e.DataTypeName - case 'n': - return e.Constraint - case 'F': - return e.File - case 'L': - return e.Line - case 'R': - return e.Routine - } - return "" -} - // ParseURL converts a url to a connection string for driver.Open. // // Deprecated: directly passing an URL to sql.Open("postgres", "postgres://...") diff --git a/error.go b/error.go index be704e923..b6a644c4d 100644 --- a/error.go +++ b/error.go @@ -6,20 +6,19 @@ import ( "io" "net" "runtime" - "strconv" - "strings" - "unicode/utf8" + + "github.com/lib/pq/pqerror" ) // [pq.Error.Severity] values. const ( - Efatal = "FATAL" - Epanic = "PANIC" - Ewarning = "WARNING" - Enotice = "NOTICE" - Edebug = "DEBUG" - Einfo = "INFO" - Elog = "LOG" + Efatal = pqerror.SeverityFatal + Epanic = pqerror.SeverityPanic + Ewarning = pqerror.SeverityWarning + Enotice = pqerror.SeverityNotice + Edebug = pqerror.SeverityDebug + Einfo = pqerror.SeverityInfo + Elog = pqerror.SeverityLog ) // Error represents an error communicating with the server. @@ -41,397 +40,16 @@ const ( // ^ // // See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields -type Error struct { - // [Efatal], [Epanic], [Ewarning], [Enotice], [Edebug], [Einfo], or [Elog]. - // Always present. - Severity string - - // SQLSTATE code. Always present. - Code ErrorCode - - // Primary human-readable error message. This should be accurate but terse - // (typically one line). Always present. - Message string - - // Optional secondary error message carrying more detail about the problem. - // Might run to multiple lines. - Detail string - - // Optional suggestion what to do about the problem. This is intended to - // differ from Detail in that it offers advice (potentially inappropriate) - // rather than hard facts. Might run to multiple lines. - Hint string - - // error position as an index into the original query string, as decimal - // ASCII integer. The first character has index 1, and positions are - // measured in characters not bytes. - Position string - - // This is defined the same as the Position field, but it is used when the - // cursor position refers to an internally generated command rather than the - // one submitted by the client. The InternalQuery field will always appear - // when this field appears. - InternalPosition string - - // Text of a failed internally-generated command. This could be, for - // example, an SQL query issued by a PL/pgSQL function. - InternalQuery string - - // An indication of the context in which the error occurred. Presently this - // includes a call stack traceback of active procedural language functions - // and internally-generated queries. The trace is one entry per line, most - // recent first. - Where string - - // If the error was associated with a specific database object, the name of - // the schema containing that object, if any. - Schema string - - // If the error was associated with a specific table, the name of the table. - // (Refer to the schema name field for the name of the table's schema.) - Table string - - // If the error was associated with a specific table column, the name of the - // column. (Refer to the schema and table name fields to identify the - // table.) - Column string - - // If the error was associated with a specific data type, the name of the - // data type. (Refer to the schema name field for the name of the data - // type's schema.) - DataTypeName string - - // If the error was associated with a specific constraint, the name of the - // constraint. Refer to fields listed above for the associated table or - // domain. (For this purpose, indexes are treated as constraints, even if - // they weren't created with constraint syntax.) - Constraint string - - // File name of the source-code location where the error was reported. - File string - - // Line number of the source-code location where the error was reported. - Line string - - // Name of the source-code routine reporting the error. - Routine string - - query string -} +type Error = pqerror.Error // ErrorCode is a five-character error code. -type ErrorCode string - -// Name returns a more human friendly rendering of the error code, namely the -// "condition name". -// -// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for -// details. -func (ec ErrorCode) Name() string { - return errorCodeNames[ec] -} +type ErrorCode = pqerror.ErrorCode // ErrorClass is only the class part of an error code. -type ErrorClass string - -// Name returns the condition name of an error class. It is equivalent to the -// condition name of the "standard" error code (i.e. the one having the last -// three characters "000"). -func (ec ErrorClass) Name() string { - return errorCodeNames[ErrorCode(ec+"000")] -} - -// Class returns the error class, e.g. "28". -// -// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for -// details. -func (ec ErrorCode) Class() ErrorClass { - return ErrorClass(ec[0:2]) -} - -// errorCodeNames is a mapping between the five-character error codes and the -// human readable "condition names". It is derived from the list at -// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html -var errorCodeNames = map[ErrorCode]string{ - // Class 00 - Successful Completion - "00000": "successful_completion", - // Class 01 - Warning - "01000": "warning", - "0100C": "dynamic_result_sets_returned", - "01008": "implicit_zero_bit_padding", - "01003": "null_value_eliminated_in_set_function", - "01007": "privilege_not_granted", - "01006": "privilege_not_revoked", - "01004": "string_data_right_truncation", - "01P01": "deprecated_feature", - // Class 02 - No Data (this is also a warning class per the SQL standard) - "02000": "no_data", - "02001": "no_additional_dynamic_result_sets_returned", - // Class 03 - SQL Statement Not Yet Complete - "03000": "sql_statement_not_yet_complete", - // Class 08 - Connection Exception - "08000": "connection_exception", - "08003": "connection_does_not_exist", - "08006": "connection_failure", - "08001": "sqlclient_unable_to_establish_sqlconnection", - "08004": "sqlserver_rejected_establishment_of_sqlconnection", - "08007": "transaction_resolution_unknown", - "08P01": "protocol_violation", - // Class 09 - Triggered Action Exception - "09000": "triggered_action_exception", - // Class 0A - Feature Not Supported - "0A000": "feature_not_supported", - // Class 0B - Invalid Transaction Initiation - "0B000": "invalid_transaction_initiation", - // Class 0F - Locator Exception - "0F000": "locator_exception", - "0F001": "invalid_locator_specification", - // Class 0L - Invalid Grantor - "0L000": "invalid_grantor", - "0LP01": "invalid_grant_operation", - // Class 0P - Invalid Role Specification - "0P000": "invalid_role_specification", - // Class 0Z - Diagnostics Exception - "0Z000": "diagnostics_exception", - "0Z002": "stacked_diagnostics_accessed_without_active_handler", - // Class 20 - Case Not Found - "20000": "case_not_found", - // Class 21 - Cardinality Violation - "21000": "cardinality_violation", - // Class 22 - Data Exception - "22000": "data_exception", - "2202E": "array_subscript_error", - "22021": "character_not_in_repertoire", - "22008": "datetime_field_overflow", - "22012": "division_by_zero", - "22005": "error_in_assignment", - "2200B": "escape_character_conflict", - "22022": "indicator_overflow", - "22015": "interval_field_overflow", - "2201E": "invalid_argument_for_logarithm", - "22014": "invalid_argument_for_ntile_function", - "22016": "invalid_argument_for_nth_value_function", - "2201F": "invalid_argument_for_power_function", - "2201G": "invalid_argument_for_width_bucket_function", - "22018": "invalid_character_value_for_cast", - "22007": "invalid_datetime_format", - "22019": "invalid_escape_character", - "2200D": "invalid_escape_octet", - "22025": "invalid_escape_sequence", - "22P06": "nonstandard_use_of_escape_character", - "22010": "invalid_indicator_parameter_value", - "22023": "invalid_parameter_value", - "2201B": "invalid_regular_expression", - "2201W": "invalid_row_count_in_limit_clause", - "2201X": "invalid_row_count_in_result_offset_clause", - "22009": "invalid_time_zone_displacement_value", - "2200C": "invalid_use_of_escape_character", - "2200G": "most_specific_type_mismatch", - "22004": "null_value_not_allowed", - "22002": "null_value_no_indicator_parameter", - "22003": "numeric_value_out_of_range", - "2200H": "sequence_generator_limit_exceeded", - "22026": "string_data_length_mismatch", - "22001": "string_data_right_truncation", - "22011": "substring_error", - "22027": "trim_error", - "22024": "unterminated_c_string", - "2200F": "zero_length_character_string", - "22P01": "floating_point_exception", - "22P02": "invalid_text_representation", - "22P03": "invalid_binary_representation", - "22P04": "bad_copy_file_format", - "22P05": "untranslatable_character", - "2200L": "not_an_xml_document", - "2200M": "invalid_xml_document", - "2200N": "invalid_xml_content", - "2200S": "invalid_xml_comment", - "2200T": "invalid_xml_processing_instruction", - // Class 23 - Integrity Constraint Violation - "23000": "integrity_constraint_violation", - "23001": "restrict_violation", - "23502": "not_null_violation", - "23503": "foreign_key_violation", - "23505": "unique_violation", - "23514": "check_violation", - "23P01": "exclusion_violation", - // Class 24 - Invalid Cursor State - "24000": "invalid_cursor_state", - // Class 25 - Invalid Transaction State - "25000": "invalid_transaction_state", - "25001": "active_sql_transaction", - "25002": "branch_transaction_already_active", - "25008": "held_cursor_requires_same_isolation_level", - "25003": "inappropriate_access_mode_for_branch_transaction", - "25004": "inappropriate_isolation_level_for_branch_transaction", - "25005": "no_active_sql_transaction_for_branch_transaction", - "25006": "read_only_sql_transaction", - "25007": "schema_and_data_statement_mixing_not_supported", - "25P01": "no_active_sql_transaction", - "25P02": "in_failed_sql_transaction", - // Class 26 - Invalid SQL Statement Name - "26000": "invalid_sql_statement_name", - // Class 27 - Triggered Data Change Violation - "27000": "triggered_data_change_violation", - // Class 28 - Invalid Authorization Specification - "28000": "invalid_authorization_specification", - "28P01": "invalid_password", - // Class 2B - Dependent Privilege Descriptors Still Exist - "2B000": "dependent_privilege_descriptors_still_exist", - "2BP01": "dependent_objects_still_exist", - // Class 2D - Invalid Transaction Termination - "2D000": "invalid_transaction_termination", - // Class 2F - SQL Routine Exception - "2F000": "sql_routine_exception", - "2F005": "function_executed_no_return_statement", - "2F002": "modifying_sql_data_not_permitted", - "2F003": "prohibited_sql_statement_attempted", - "2F004": "reading_sql_data_not_permitted", - // Class 34 - Invalid Cursor Name - "34000": "invalid_cursor_name", - // Class 38 - External Routine Exception - "38000": "external_routine_exception", - "38001": "containing_sql_not_permitted", - "38002": "modifying_sql_data_not_permitted", - "38003": "prohibited_sql_statement_attempted", - "38004": "reading_sql_data_not_permitted", - // Class 39 - External Routine Invocation Exception - "39000": "external_routine_invocation_exception", - "39001": "invalid_sqlstate_returned", - "39004": "null_value_not_allowed", - "39P01": "trigger_protocol_violated", - "39P02": "srf_protocol_violated", - // Class 3B - Savepoint Exception - "3B000": "savepoint_exception", - "3B001": "invalid_savepoint_specification", - // Class 3D - Invalid Catalog Name - "3D000": "invalid_catalog_name", - // Class 3F - Invalid Schema Name - "3F000": "invalid_schema_name", - // Class 40 - Transaction Rollback - "40000": "transaction_rollback", - "40002": "transaction_integrity_constraint_violation", - "40001": "serialization_failure", - "40003": "statement_completion_unknown", - "40P01": "deadlock_detected", - // Class 42 - Syntax Error or Access Rule Violation - "42000": "syntax_error_or_access_rule_violation", - "42601": "syntax_error", - "42501": "insufficient_privilege", - "42846": "cannot_coerce", - "42803": "grouping_error", - "42P20": "windowing_error", - "42P19": "invalid_recursion", - "42830": "invalid_foreign_key", - "42602": "invalid_name", - "42622": "name_too_long", - "42939": "reserved_name", - "42804": "datatype_mismatch", - "42P18": "indeterminate_datatype", - "42P21": "collation_mismatch", - "42P22": "indeterminate_collation", - "42809": "wrong_object_type", - "42703": "undefined_column", - "42883": "undefined_function", - "42P01": "undefined_table", - "42P02": "undefined_parameter", - "42704": "undefined_object", - "42701": "duplicate_column", - "42P03": "duplicate_cursor", - "42P04": "duplicate_database", - "42723": "duplicate_function", - "42P05": "duplicate_prepared_statement", - "42P06": "duplicate_schema", - "42P07": "duplicate_table", - "42712": "duplicate_alias", - "42710": "duplicate_object", - "42702": "ambiguous_column", - "42725": "ambiguous_function", - "42P08": "ambiguous_parameter", - "42P09": "ambiguous_alias", - "42P10": "invalid_column_reference", - "42611": "invalid_column_definition", - "42P11": "invalid_cursor_definition", - "42P12": "invalid_database_definition", - "42P13": "invalid_function_definition", - "42P14": "invalid_prepared_statement_definition", - "42P15": "invalid_schema_definition", - "42P16": "invalid_table_definition", - "42P17": "invalid_object_definition", - // Class 44 - WITH CHECK OPTION Violation - "44000": "with_check_option_violation", - // Class 53 - Insufficient Resources - "53000": "insufficient_resources", - "53100": "disk_full", - "53200": "out_of_memory", - "53300": "too_many_connections", - "53400": "configuration_limit_exceeded", - // Class 54 - Program Limit Exceeded - "54000": "program_limit_exceeded", - "54001": "statement_too_complex", - "54011": "too_many_columns", - "54023": "too_many_arguments", - // Class 55 - Object Not In Prerequisite State - "55000": "object_not_in_prerequisite_state", - "55006": "object_in_use", - "55P02": "cant_change_runtime_param", - "55P03": "lock_not_available", - // Class 57 - Operator Intervention - "57000": "operator_intervention", - "57014": "query_canceled", - "57P01": "admin_shutdown", - "57P02": "crash_shutdown", - "57P03": "cannot_connect_now", - "57P04": "database_dropped", - // Class 58 - System Error (errors external to PostgreSQL itself) - "58000": "system_error", - "58030": "io_error", - "58P01": "undefined_file", - "58P02": "duplicate_file", - // Class F0 - Configuration File Error - "F0000": "config_file_error", - "F0001": "lock_file_exists", - // Class HV - Foreign Data Wrapper Error (SQL/MED) - "HV000": "fdw_error", - "HV005": "fdw_column_name_not_found", - "HV002": "fdw_dynamic_parameter_value_needed", - "HV010": "fdw_function_sequence_error", - "HV021": "fdw_inconsistent_descriptor_information", - "HV024": "fdw_invalid_attribute_value", - "HV007": "fdw_invalid_column_name", - "HV008": "fdw_invalid_column_number", - "HV004": "fdw_invalid_data_type", - "HV006": "fdw_invalid_data_type_descriptors", - "HV091": "fdw_invalid_descriptor_field_identifier", - "HV00B": "fdw_invalid_handle", - "HV00C": "fdw_invalid_option_index", - "HV00D": "fdw_invalid_option_name", - "HV090": "fdw_invalid_string_length_or_buffer_length", - "HV00A": "fdw_invalid_string_format", - "HV009": "fdw_invalid_use_of_null_pointer", - "HV014": "fdw_too_many_handles", - "HV001": "fdw_out_of_memory", - "HV00P": "fdw_no_schemas", - "HV00J": "fdw_option_name_not_found", - "HV00K": "fdw_reply_handle", - "HV00Q": "fdw_schema_not_found", - "HV00R": "fdw_table_not_found", - "HV00L": "fdw_unable_to_create_execution", - "HV00M": "fdw_unable_to_create_reply", - "HV00N": "fdw_unable_to_establish_connection", - // Class P0 - PL/pgSQL Error - "P0000": "plpgsql_error", - "P0001": "raise_exception", - "P0002": "no_data_found", - "P0003": "too_many_rows", - // Class XX - Internal Error - "XX000": "internal_error", - "XX001": "data_corrupted", - "XX002": "index_corrupted", -} +type ErrorClass = pqerror.ErrorClass func parseError(r *readBuf, q string) *Error { - err := &Error{query: q} + err := &Error{Query: q} for t := r.byte(); t != 0; t = r.byte() { msg := r.string() switch t { @@ -474,131 +92,6 @@ func parseError(r *readBuf, q string) *Error { return err } -// Fatal returns true if the Error Severity is fatal. -func (e *Error) Fatal() bool { - return e.Severity == Efatal -} - -// SQLState returns the SQLState of the error. -func (e *Error) SQLState() string { - return string(e.Code) -} - -func (e *Error) Error() string { - msg := e.Message - if e.query != "" && e.Position != "" { - pos, err := strconv.Atoi(e.Position) - if err == nil { - lines := strings.Split(e.query, "\n") - line, col := posToLine(pos, lines) - if len(lines) == 1 { - msg += " at column " + strconv.Itoa(col) - } else { - msg += " at position " + strconv.Itoa(line) + ":" + strconv.Itoa(col) - } - } - } - - if e.Code != "" { - return "pq: " + msg + " (" + string(e.Code) + ")" - } - return "pq: " + msg -} - -// ErrorWithDetail returns the error message with detailed information and -// location context (if any). -// -// See the documentation on [Error]. -func (e *Error) ErrorWithDetail() string { - b := new(strings.Builder) - b.Grow(len(e.Message) + len(e.Detail) + len(e.Hint) + 30) - b.WriteString("ERROR: ") - b.WriteString(e.Message) - if e.Code != "" { - b.WriteString(" (") - b.WriteString(string(e.Code)) - b.WriteByte(')') - } - if e.Detail != "" { - b.WriteString("\nDETAIL: ") - b.WriteString(e.Detail) - } - if e.Hint != "" { - b.WriteString("\nHINT: ") - b.WriteString(e.Hint) - } - - if e.query != "" && e.Position != "" { - b.Grow(512) - pos, err := strconv.Atoi(e.Position) - if err != nil { - return b.String() - } - lines := strings.Split(e.query, "\n") - line, col := posToLine(pos, lines) - - fmt.Fprintf(b, "\nCONTEXT: line %d, column %d:\n\n", line, col) - if line > 2 { - fmt.Fprintf(b, "% 7d | %s\n", line-2, expandTab(lines[line-3])) - } - if line > 1 { - fmt.Fprintf(b, "% 7d | %s\n", line-1, expandTab(lines[line-2])) - } - /// Expand tabs, so that the ^ is at at the correct position, but leave - /// "column 10-13" intact. Adjusting this to the visual column would be - /// better, but we don't know the tabsize of the user in their editor, - /// which can be 8, 4, 2, or something else. We can't know. So leaving - /// it as the character index is probably the "most correct". - expanded := expandTab(lines[line-1]) - diff := len(expanded) - len(lines[line-1]) - fmt.Fprintf(b, "% 7d | %s\n", line, expanded) - fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col-1+diff), "^") - } - - return b.String() -} - -func posToLine(pos int, lines []string) (line, col int) { - read := 0 - for i := range lines { - line++ - ll := utf8.RuneCountInString(lines[i]) + 1 // +1 for the removed newline - if read+ll >= pos { - col = max(pos-read, 1) // Should be lower than 1, but just in case. - break - } - read += ll - } - return line, col -} - -func expandTab(s string) string { - var ( - b strings.Builder - l int - fill = func(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = ' ' - } - return string(b) - } - ) - b.Grow(len(s)) - for _, r := range s { - switch r { - case '\t': - tw := 8 - l%8 - b.WriteString(fill(tw)) - l += tw - default: - b.WriteRune(r) - l += 1 - } - } - return b.String() -} - func (cn *conn) handleError(reported error, query ...string) error { switch err := reported.(type) { case nil: @@ -610,7 +103,7 @@ func (cn *conn) handleError(reported error, query ...string) error { reported = driver.ErrBadConn case *Error: if len(query) > 0 && query[0] != "" { - err.query = query[0] + err.Query = query[0] reported = err } if err.Fatal() { diff --git a/error_test.go b/error_test.go index c1fbb3ef8..841bb346f 100644 --- a/error_test.go +++ b/error_test.go @@ -140,7 +140,7 @@ func TestError(t *testing.T) { t.Errorf("\nhave: %s\nwant: %s", err.Error(), tt.want) } tt.wantDetail = pqtest.NormalizeIndent(tt.wantDetail) - if pqErr.query != "" && pqErr.Position != "" { + if pqErr.Query != "" && pqErr.Position != "" { tt.wantDetail += "\n" } if pqErr.ErrorWithDetail() != tt.wantDetail { diff --git a/pqerror/as.go b/pqerror/as.go new file mode 100644 index 000000000..f8d918d50 --- /dev/null +++ b/pqerror/as.go @@ -0,0 +1,25 @@ +//go:build !go1.26 + +package pqerror + +import ( + "errors" + "slices" +) + +// As asserts that the given error is [pqerror.Error] and returns it. +// +// It will return nil if the Error is not one of the given error codes. If no +// codes are given it will always return the Error. +// +// This is safe to call with a nil error. +func As(err error, codes ...ErrorCode) *Error { + if err == nil { // Not strictly needed, but prevents alloc for nil errors. + return nil + } + pqErr := new(Error) + if errors.As(err, &pqErr) && (len(codes) == 0 || slices.Contains(codes, pqErr.Code)) { + return pqErr + } + return nil +} diff --git a/pqerror/as_126.go b/pqerror/as_126.go new file mode 100644 index 000000000..eb15545df --- /dev/null +++ b/pqerror/as_126.go @@ -0,0 +1,21 @@ +//go:build go1.26 + +package pqerror + +import ( + "errors" + "slices" +) + +// As asserts that the given error is [pqerror.Error] and returns it. +// +// It will return nil if the Error is not one of the given error codes. If no +// codes are given it will always return the Error. +// +// This is safe to call with a nil error. +func As(err error, codes ...ErrorCode) *Error { + if pqErr, ok := errors.AsType[*Error](err); ok && (len(codes) == 0 || slices.Contains(codes, pqErr.Code)) { + return pqErr + } + return nil +} diff --git a/pqerror/codes.go b/pqerror/codes.go new file mode 100644 index 000000000..9c6781b3d --- /dev/null +++ b/pqerror/codes.go @@ -0,0 +1,581 @@ +// Code generated by gen.go. DO NOT EDIT. + +// Last updated for PostgreSQL 18.3 + +package pqerror + +var ( + ClassSuccessfulCompletion = ErrorClass("00") // Successful Completion + ClassWarning = ErrorClass("01") // Warning + ClassNoData = ErrorClass("02") // No Data (this is also a warning class per the SQL standard) + ClassSQLStatementNotYetComplete = ErrorClass("03") // SQL Statement Not Yet Complete + ClassConnectionException = ErrorClass("08") // Connection Exception + ClassTriggeredActionException = ErrorClass("09") // Triggered Action Exception + ClassFeatureNotSupported = ErrorClass("0A") // Feature Not Supported + ClassInvalidTransactionInitiation = ErrorClass("0B") // Invalid Transaction Initiation + ClassLocatorException = ErrorClass("0F") // Locator Exception + ClassInvalidGrantor = ErrorClass("0L") // Invalid Grantor + ClassInvalidRoleSpecification = ErrorClass("0P") // Invalid Role Specification + ClassDiagnosticsException = ErrorClass("0Z") // Diagnostics Exception + ClassCaseNotFound = ErrorClass("20") // Case Not Found + ClassCardinalityViolation = ErrorClass("21") // Cardinality Violation + ClassDataException = ErrorClass("22") // Data Exception + ClassIntegrityConstraintViolation = ErrorClass("23") // Integrity Constraint Violation + ClassInvalidCursorState = ErrorClass("24") // Invalid Cursor State + ClassInvalidTransactionState = ErrorClass("25") // Invalid Transaction State + ClassInvalidSQLStatementName = ErrorClass("26") // Invalid SQL Statement Name + ClassTriggeredDataChangeViolation = ErrorClass("27") // Triggered Data Change Violation + ClassInvalidAuthorizationSpecification = ErrorClass("28") // Invalid Authorization Specification + ClassDependentPrivilegeDescriptorsStillExist = ErrorClass("2B") // Dependent Privilege Descriptors Still Exist + ClassInvalidTransactionTermination = ErrorClass("2D") // Invalid Transaction Termination + ClassSQLRoutineException = ErrorClass("2F") // SQL Routine Exception + ClassInvalidCursorName = ErrorClass("34") // Invalid Cursor Name + ClassExternalRoutineException = ErrorClass("38") // External Routine Exception + ClassExternalRoutineInvocationException = ErrorClass("39") // External Routine Invocation Exception + ClassSavepointException = ErrorClass("3B") // Savepoint Exception + ClassInvalidCatalogName = ErrorClass("3D") // Invalid Catalog Name + ClassInvalidSchemaName = ErrorClass("3F") // Invalid Schema Name + ClassTransactionRollback = ErrorClass("40") // Transaction Rollback + ClassSyntaxErrorOrAccessRuleViolation = ErrorClass("42") // Syntax Error or Access Rule Violation + ClassWithCheckOptionViolation = ErrorClass("44") // WITH CHECK OPTION Violation + ClassInsufficientResources = ErrorClass("53") // Insufficient Resources + ClassProgramLimitExceeded = ErrorClass("54") // Program Limit Exceeded + ClassObjectNotInPrerequisiteState = ErrorClass("55") // Object Not In Prerequisite State + ClassOperatorIntervention = ErrorClass("57") // Operator Intervention + ClassSystemError = ErrorClass("58") // System Error (errors external to PostgreSQL itself) + ClassConfigFileError = ErrorClass("F0") // Configuration File Error + ClassFDWError = ErrorClass("HV") // Foreign Data Wrapper Error (SQL/MED) + ClassPLpgSQLError = ErrorClass("P0") // PL/pgSQL Error + ClassInternalError = ErrorClass("XX") // Internal Error +) + +// A list of all error codes used in PostgreSQL. +var ( + SuccessfulCompletion = ErrorCode("00000") // Class 00 - Successful Completion + Warning = ErrorCode("01000") // Class 01 - Warning + WarningDynamicResultSetsReturned = ErrorCode("0100C") + WarningImplicitZeroBitPadding = ErrorCode("01008") + WarningNullValueEliminatedInSetFunction = ErrorCode("01003") + WarningPrivilegeNotGranted = ErrorCode("01007") + WarningPrivilegeNotRevoked = ErrorCode("01006") + WarningStringDataRightTruncation = ErrorCode("01004") + WarningDeprecatedFeature = ErrorCode("01P01") + NoData = ErrorCode("02000") // Class 02 - No Data (this is also a warning class per the SQL standard) + NoAdditionalDynamicResultSetsReturned = ErrorCode("02001") + SQLStatementNotYetComplete = ErrorCode("03000") // Class 03 - SQL Statement Not Yet Complete + ConnectionException = ErrorCode("08000") // Class 08 - Connection Exception + ConnectionDoesNotExist = ErrorCode("08003") + ConnectionFailure = ErrorCode("08006") + SQLClientUnableToEstablishSQLConnection = ErrorCode("08001") + SQLServerRejectedEstablishmentOfSQLConnection = ErrorCode("08004") + TransactionResolutionUnknown = ErrorCode("08007") + ProtocolViolation = ErrorCode("08P01") + TriggeredActionException = ErrorCode("09000") // Class 09 - Triggered Action Exception + FeatureNotSupported = ErrorCode("0A000") // Class 0A - Feature Not Supported + InvalidTransactionInitiation = ErrorCode("0B000") // Class 0B - Invalid Transaction Initiation + LocatorException = ErrorCode("0F000") // Class 0F - Locator Exception + LEInvalidSpecification = ErrorCode("0F001") + InvalidGrantor = ErrorCode("0L000") // Class 0L - Invalid Grantor + InvalidGrantOperation = ErrorCode("0LP01") + InvalidRoleSpecification = ErrorCode("0P000") // Class 0P - Invalid Role Specification + DiagnosticsException = ErrorCode("0Z000") // Class 0Z - Diagnostics Exception + StackedDiagnosticsAccessedWithoutActiveHandler = ErrorCode("0Z002") + InvalidArgumentForXquery = ErrorCode("10608") + CaseNotFound = ErrorCode("20000") // Class 20 - Case Not Found + CardinalityViolation = ErrorCode("21000") // Class 21 - Cardinality Violation + DataException = ErrorCode("22000") // Class 22 - Data Exception + ArraySubscriptError = ErrorCode("2202E") + CharacterNotInRepertoire = ErrorCode("22021") + DatetimeFieldOverflow = ErrorCode("22008") + DivisionByZero = ErrorCode("22012") + ErrorInAssignment = ErrorCode("22005") + EscapeCharacterConflict = ErrorCode("2200B") + IndicatorOverflow = ErrorCode("22022") + IntervalFieldOverflow = ErrorCode("22015") + InvalidArgumentForLog = ErrorCode("2201E") + InvalidArgumentForNtile = ErrorCode("22014") + InvalidArgumentForNthValue = ErrorCode("22016") + InvalidArgumentForPowerFunction = ErrorCode("2201F") + InvalidArgumentForWidthBucketFunction = ErrorCode("2201G") + InvalidCharacterValueForCast = ErrorCode("22018") + InvalidDatetimeFormat = ErrorCode("22007") + InvalidEscapeCharacter = ErrorCode("22019") + InvalidEscapeOctet = ErrorCode("2200D") + InvalidEscapeSequence = ErrorCode("22025") + NonstandardUseOfEscapeCharacter = ErrorCode("22P06") + InvalidIndicatorParameterValue = ErrorCode("22010") + InvalidParameterValue = ErrorCode("22023") + InvalidPrecedingOrFollowingSize = ErrorCode("22013") + InvalidRegularExpression = ErrorCode("2201B") + InvalidRowCountInLimitClause = ErrorCode("2201W") + InvalidRowCountInResultOffsetClause = ErrorCode("2201X") + InvalidTablesampleArgument = ErrorCode("2202H") + InvalidTablesampleRepeat = ErrorCode("2202G") + InvalidTimeZoneDisplacementValue = ErrorCode("22009") + InvalidUseOfEscapeCharacter = ErrorCode("2200C") + MostSpecificTypeMismatch = ErrorCode("2200G") + NullValueNotAllowed = ErrorCode("22004") + NullValueNoIndicatorParameter = ErrorCode("22002") + NumericValueOutOfRange = ErrorCode("22003") + SequenceGeneratorLimitExceeded = ErrorCode("2200H") + StringDataLengthMismatch = ErrorCode("22026") + StringDataRightTruncation = ErrorCode("22001") + SubstringError = ErrorCode("22011") + TrimError = ErrorCode("22027") + UnterminatedCString = ErrorCode("22024") + ZeroLengthCharacterString = ErrorCode("2200F") + FloatingPointException = ErrorCode("22P01") + InvalidTextRepresentation = ErrorCode("22P02") + InvalidBinaryRepresentation = ErrorCode("22P03") + BadCopyFileFormat = ErrorCode("22P04") + UntranslatableCharacter = ErrorCode("22P05") + NotAnXMLDocument = ErrorCode("2200L") + InvalidXMLDocument = ErrorCode("2200M") + InvalidXMLContent = ErrorCode("2200N") + InvalidXMLComment = ErrorCode("2200S") + InvalidXMLProcessingInstruction = ErrorCode("2200T") + DuplicateJSONObjectKeyValue = ErrorCode("22030") + InvalidArgumentForSQLJSONDatetimeFunction = ErrorCode("22031") + InvalidJSONText = ErrorCode("22032") + InvalidSQLJSONSubscript = ErrorCode("22033") + MoreThanOneSQLJSONItem = ErrorCode("22034") + NoSQLJSONItem = ErrorCode("22035") + NonNumericSQLJSONItem = ErrorCode("22036") + NonUniqueKeysInAJSONObject = ErrorCode("22037") + SingletonSQLJSONItemRequired = ErrorCode("22038") + SQLJSONArrayNotFound = ErrorCode("22039") + SQLJSONMemberNotFound = ErrorCode("2203A") + SQLJSONNumberNotFound = ErrorCode("2203B") + SQLJSONObjectNotFound = ErrorCode("2203C") + TooManyJSONArrayElements = ErrorCode("2203D") + TooManyJSONObjectMembers = ErrorCode("2203E") + SQLJSONScalarRequired = ErrorCode("2203F") + SQLJSONItemCannotBeCastToTargetType = ErrorCode("2203G") + IntegrityConstraintViolation = ErrorCode("23000") // Class 23 - Integrity Constraint Violation + RestrictViolation = ErrorCode("23001") + NotNullViolation = ErrorCode("23502") + ForeignKeyViolation = ErrorCode("23503") + UniqueViolation = ErrorCode("23505") + CheckViolation = ErrorCode("23514") + ExclusionViolation = ErrorCode("23P01") + InvalidCursorState = ErrorCode("24000") // Class 24 - Invalid Cursor State + InvalidTransactionState = ErrorCode("25000") // Class 25 - Invalid Transaction State + ActiveSQLTransaction = ErrorCode("25001") + BranchTransactionAlreadyActive = ErrorCode("25002") + HeldCursorRequiresSameIsolationLevel = ErrorCode("25008") + InappropriateAccessModeForBranchTransaction = ErrorCode("25003") + InappropriateIsolationLevelForBranchTransaction = ErrorCode("25004") + NoActiveSQLTransactionForBranchTransaction = ErrorCode("25005") + ReadOnlySQLTransaction = ErrorCode("25006") + SchemaAndDataStatementMixingNotSupported = ErrorCode("25007") + NoActiveSQLTransaction = ErrorCode("25P01") + InFailedSQLTransaction = ErrorCode("25P02") + IdleInTransactionSessionTimeout = ErrorCode("25P03") + TransactionTimeout = ErrorCode("25P04") + InvalidSQLStatementName = ErrorCode("26000") // Class 26 - Invalid SQL Statement Name + TriggeredDataChangeViolation = ErrorCode("27000") // Class 27 - Triggered Data Change Violation + InvalidAuthorizationSpecification = ErrorCode("28000") // Class 28 - Invalid Authorization Specification + InvalidPassword = ErrorCode("28P01") + DependentPrivilegeDescriptorsStillExist = ErrorCode("2B000") // Class 2B - Dependent Privilege Descriptors Still Exist + DependentObjectsStillExist = ErrorCode("2BP01") + InvalidTransactionTermination = ErrorCode("2D000") // Class 2D - Invalid Transaction Termination + SQLRoutineException = ErrorCode("2F000") // Class 2F - SQL Routine Exception + SREFunctionExecutedNoReturnStatement = ErrorCode("2F005") + SREModifyingSQLDataNotPermitted = ErrorCode("2F002") + SREProhibitedSQLStatementAttempted = ErrorCode("2F003") + SREReadingSQLDataNotPermitted = ErrorCode("2F004") + InvalidCursorName = ErrorCode("34000") // Class 34 - Invalid Cursor Name + ExternalRoutineException = ErrorCode("38000") // Class 38 - External Routine Exception + EREContainingSQLNotPermitted = ErrorCode("38001") + EREModifyingSQLDataNotPermitted = ErrorCode("38002") + EREProhibitedSQLStatementAttempted = ErrorCode("38003") + EREReadingSQLDataNotPermitted = ErrorCode("38004") + ExternalRoutineInvocationException = ErrorCode("39000") // Class 39 - External Routine Invocation Exception + ERIEInvalidSQLSTATEReturned = ErrorCode("39001") + ERIENullValueNotAllowed = ErrorCode("39004") + ERIETriggerProtocolViolated = ErrorCode("39P01") + ERIESrfProtocolViolated = ErrorCode("39P02") + ERIEEventTriggerProtocolViolated = ErrorCode("39P03") + SavepointException = ErrorCode("3B000") // Class 3B - Savepoint Exception + SEInvalidSpecification = ErrorCode("3B001") + InvalidCatalogName = ErrorCode("3D000") // Class 3D - Invalid Catalog Name + InvalidSchemaName = ErrorCode("3F000") // Class 3F - Invalid Schema Name + TransactionRollback = ErrorCode("40000") // Class 40 - Transaction Rollback + TRIntegrityConstraintViolation = ErrorCode("40002") + TRSerializationFailure = ErrorCode("40001") + TRStatementCompletionUnknown = ErrorCode("40003") + TRDeadlockDetected = ErrorCode("40P01") + SyntaxErrorOrAccessRuleViolation = ErrorCode("42000") // Class 42 - Syntax Error or Access Rule Violation + SyntaxError = ErrorCode("42601") + InsufficientPrivilege = ErrorCode("42501") + CannotCoerce = ErrorCode("42846") + GroupingError = ErrorCode("42803") + WindowingError = ErrorCode("42P20") + InvalidRecursion = ErrorCode("42P19") + InvalidForeignKey = ErrorCode("42830") + InvalidName = ErrorCode("42602") + NameTooLong = ErrorCode("42622") + ReservedName = ErrorCode("42939") + DatatypeMismatch = ErrorCode("42804") + IndeterminateDatatype = ErrorCode("42P18") + CollationMismatch = ErrorCode("42P21") + IndeterminateCollation = ErrorCode("42P22") + WrongObjectType = ErrorCode("42809") + GeneratedAlways = ErrorCode("428C9") + UndefinedColumn = ErrorCode("42703") + UndefinedFunction = ErrorCode("42883") + UndefinedTable = ErrorCode("42P01") + UndefinedParameter = ErrorCode("42P02") + UndefinedObject = ErrorCode("42704") + DuplicateColumn = ErrorCode("42701") + DuplicateCursor = ErrorCode("42P03") + DuplicateDatabase = ErrorCode("42P04") + DuplicateFunction = ErrorCode("42723") + DuplicatePstatement = ErrorCode("42P05") + DuplicateSchema = ErrorCode("42P06") + DuplicateTable = ErrorCode("42P07") + DuplicateAlias = ErrorCode("42712") + DuplicateObject = ErrorCode("42710") + AmbiguousColumn = ErrorCode("42702") + AmbiguousFunction = ErrorCode("42725") + AmbiguousParameter = ErrorCode("42P08") + AmbiguousAlias = ErrorCode("42P09") + InvalidColumnReference = ErrorCode("42P10") + InvalidColumnDefinition = ErrorCode("42611") + InvalidCursorDefinition = ErrorCode("42P11") + InvalidDatabaseDefinition = ErrorCode("42P12") + InvalidFunctionDefinition = ErrorCode("42P13") + InvalidPstatementDefinition = ErrorCode("42P14") + InvalidSchemaDefinition = ErrorCode("42P15") + InvalidTableDefinition = ErrorCode("42P16") + InvalidObjectDefinition = ErrorCode("42P17") + WithCheckOptionViolation = ErrorCode("44000") // Class 44 - WITH CHECK OPTION Violation + InsufficientResources = ErrorCode("53000") // Class 53 - Insufficient Resources + DiskFull = ErrorCode("53100") + OutOfMemory = ErrorCode("53200") + TooManyConnections = ErrorCode("53300") + ConfigurationLimitExceeded = ErrorCode("53400") + ProgramLimitExceeded = ErrorCode("54000") // Class 54 - Program Limit Exceeded + StatementTooComplex = ErrorCode("54001") + TooManyColumns = ErrorCode("54011") + TooManyArguments = ErrorCode("54023") + ObjectNotInPrerequisiteState = ErrorCode("55000") // Class 55 - Object Not In Prerequisite State + ObjectInUse = ErrorCode("55006") + CantChangeRuntimeParam = ErrorCode("55P02") + LockNotAvailable = ErrorCode("55P03") + UnsafeNewEnumValueUsage = ErrorCode("55P04") + OperatorIntervention = ErrorCode("57000") // Class 57 - Operator Intervention + QueryCanceled = ErrorCode("57014") + AdminShutdown = ErrorCode("57P01") + CrashShutdown = ErrorCode("57P02") + CannotConnectNow = ErrorCode("57P03") + DatabaseDropped = ErrorCode("57P04") + IdleSessionTimeout = ErrorCode("57P05") + SystemError = ErrorCode("58000") // Class 58 - System Error (errors external to PostgreSQL itself) + IOError = ErrorCode("58030") + UndefinedFile = ErrorCode("58P01") + DuplicateFile = ErrorCode("58P02") + FileNameTooLong = ErrorCode("58P03") + ConfigFileError = ErrorCode("F0000") // Class F0 - Configuration File Error + LockFileExists = ErrorCode("F0001") + FDWError = ErrorCode("HV000") // Class HV - Foreign Data Wrapper Error (SQL/MED) + FDWColumnNameNotFound = ErrorCode("HV005") + FDWDynamicParameterValueNeeded = ErrorCode("HV002") + FDWFunctionSequenceError = ErrorCode("HV010") + FDWInconsistentDescriptorInformation = ErrorCode("HV021") + FDWInvalidAttributeValue = ErrorCode("HV024") + FDWInvalidColumnName = ErrorCode("HV007") + FDWInvalidColumnNumber = ErrorCode("HV008") + FDWInvalidDataType = ErrorCode("HV004") + FDWInvalidDataTypeDescriptors = ErrorCode("HV006") + FDWInvalidDescriptorFieldIdentifier = ErrorCode("HV091") + FDWInvalidHandle = ErrorCode("HV00B") + FDWInvalidOptionIndex = ErrorCode("HV00C") + FDWInvalidOptionName = ErrorCode("HV00D") + FDWInvalidStringLengthOrBufferLength = ErrorCode("HV090") + FDWInvalidStringFormat = ErrorCode("HV00A") + FDWInvalidUseOfNullPointer = ErrorCode("HV009") + FDWTooManyHandles = ErrorCode("HV014") + FDWOutOfMemory = ErrorCode("HV001") + FDWNoSchemas = ErrorCode("HV00P") + FDWOptionNameNotFound = ErrorCode("HV00J") + FDWReplyHandle = ErrorCode("HV00K") + FDWSchemaNotFound = ErrorCode("HV00Q") + FDWTableNotFound = ErrorCode("HV00R") + FDWUnableToCreateExecution = ErrorCode("HV00L") + FDWUnableToCreateReply = ErrorCode("HV00M") + FDWUnableToEstablishConnection = ErrorCode("HV00N") + PLpgSQLError = ErrorCode("P0000") // Class P0 - PL/pgSQL Error + RaiseException = ErrorCode("P0001") + NoDataFound = ErrorCode("P0002") + TooManyRows = ErrorCode("P0003") + AssertFailure = ErrorCode("P0004") + InternalError = ErrorCode("XX000") // Class XX - Internal Error + DataCorrupted = ErrorCode("XX001") + IndexCorrupted = ErrorCode("XX002") +) + +var errorCodeNames = map[ErrorCode]string{ + "00000": "successful_completion", + "01000": "warning", + "0100C": "dynamic_result_sets_returned", + "01008": "implicit_zero_bit_padding", + "01003": "null_value_eliminated_in_set_function", + "01007": "privilege_not_granted", + "01006": "privilege_not_revoked", + "01004": "string_data_right_truncation", + "01P01": "deprecated_feature", + "02000": "no_data", + "02001": "no_additional_dynamic_result_sets_returned", + "03000": "sql_statement_not_yet_complete", + "08000": "connection_exception", + "08003": "connection_does_not_exist", + "08006": "connection_failure", + "08001": "sqlclient_unable_to_establish_sqlconnection", + "08004": "sqlserver_rejected_establishment_of_sqlconnection", + "08007": "transaction_resolution_unknown", + "08P01": "protocol_violation", + "09000": "triggered_action_exception", + "0A000": "feature_not_supported", + "0B000": "invalid_transaction_initiation", + "0F000": "locator_exception", + "0F001": "invalid_locator_specification", + "0L000": "invalid_grantor", + "0LP01": "invalid_grant_operation", + "0P000": "invalid_role_specification", + "0Z000": "diagnostics_exception", + "0Z002": "stacked_diagnostics_accessed_without_active_handler", + "10608": "invalid_argument_for_xquery", + "20000": "case_not_found", + "21000": "cardinality_violation", + "22000": "data_exception", + "2202E": "array_subscript_error", + "22021": "character_not_in_repertoire", + "22008": "datetime_field_overflow", + "22012": "division_by_zero", + "22005": "error_in_assignment", + "2200B": "escape_character_conflict", + "22022": "indicator_overflow", + "22015": "interval_field_overflow", + "2201E": "invalid_argument_for_logarithm", + "22014": "invalid_argument_for_ntile_function", + "22016": "invalid_argument_for_nth_value_function", + "2201F": "invalid_argument_for_power_function", + "2201G": "invalid_argument_for_width_bucket_function", + "22018": "invalid_character_value_for_cast", + "22007": "invalid_datetime_format", + "22019": "invalid_escape_character", + "2200D": "invalid_escape_octet", + "22025": "invalid_escape_sequence", + "22P06": "nonstandard_use_of_escape_character", + "22010": "invalid_indicator_parameter_value", + "22023": "invalid_parameter_value", + "22013": "invalid_preceding_or_following_size", + "2201B": "invalid_regular_expression", + "2201W": "invalid_row_count_in_limit_clause", + "2201X": "invalid_row_count_in_result_offset_clause", + "2202H": "invalid_tablesample_argument", + "2202G": "invalid_tablesample_repeat", + "22009": "invalid_time_zone_displacement_value", + "2200C": "invalid_use_of_escape_character", + "2200G": "most_specific_type_mismatch", + "22004": "null_value_not_allowed", + "22002": "null_value_no_indicator_parameter", + "22003": "numeric_value_out_of_range", + "2200H": "sequence_generator_limit_exceeded", + "22026": "string_data_length_mismatch", + "22001": "string_data_right_truncation", + "22011": "substring_error", + "22027": "trim_error", + "22024": "unterminated_c_string", + "2200F": "zero_length_character_string", + "22P01": "floating_point_exception", + "22P02": "invalid_text_representation", + "22P03": "invalid_binary_representation", + "22P04": "bad_copy_file_format", + "22P05": "untranslatable_character", + "2200L": "not_an_xml_document", + "2200M": "invalid_xml_document", + "2200N": "invalid_xml_content", + "2200S": "invalid_xml_comment", + "2200T": "invalid_xml_processing_instruction", + "22030": "duplicate_json_object_key_value", + "22031": "invalid_argument_for_sql_json_datetime_function", + "22032": "invalid_json_text", + "22033": "invalid_sql_json_subscript", + "22034": "more_than_one_sql_json_item", + "22035": "no_sql_json_item", + "22036": "non_numeric_sql_json_item", + "22037": "non_unique_keys_in_a_json_object", + "22038": "singleton_sql_json_item_required", + "22039": "sql_json_array_not_found", + "2203A": "sql_json_member_not_found", + "2203B": "sql_json_number_not_found", + "2203C": "sql_json_object_not_found", + "2203D": "too_many_json_array_elements", + "2203E": "too_many_json_object_members", + "2203F": "sql_json_scalar_required", + "2203G": "sql_json_item_cannot_be_cast_to_target_type", + "23000": "integrity_constraint_violation", + "23001": "restrict_violation", + "23502": "not_null_violation", + "23503": "foreign_key_violation", + "23505": "unique_violation", + "23514": "check_violation", + "23P01": "exclusion_violation", + "24000": "invalid_cursor_state", + "25000": "invalid_transaction_state", + "25001": "active_sql_transaction", + "25002": "branch_transaction_already_active", + "25008": "held_cursor_requires_same_isolation_level", + "25003": "inappropriate_access_mode_for_branch_transaction", + "25004": "inappropriate_isolation_level_for_branch_transaction", + "25005": "no_active_sql_transaction_for_branch_transaction", + "25006": "read_only_sql_transaction", + "25007": "schema_and_data_statement_mixing_not_supported", + "25P01": "no_active_sql_transaction", + "25P02": "in_failed_sql_transaction", + "25P03": "idle_in_transaction_session_timeout", + "25P04": "transaction_timeout", + "26000": "invalid_sql_statement_name", + "27000": "triggered_data_change_violation", + "28000": "invalid_authorization_specification", + "28P01": "invalid_password", + "2B000": "dependent_privilege_descriptors_still_exist", + "2BP01": "dependent_objects_still_exist", + "2D000": "invalid_transaction_termination", + "2F000": "sql_routine_exception", + "2F005": "function_executed_no_return_statement", + "2F002": "modifying_sql_data_not_permitted", + "2F003": "prohibited_sql_statement_attempted", + "2F004": "reading_sql_data_not_permitted", + "34000": "invalid_cursor_name", + "38000": "external_routine_exception", + "38001": "containing_sql_not_permitted", + "38002": "modifying_sql_data_not_permitted", + "38003": "prohibited_sql_statement_attempted", + "38004": "reading_sql_data_not_permitted", + "39000": "external_routine_invocation_exception", + "39001": "invalid_sqlstate_returned", + "39004": "null_value_not_allowed", + "39P01": "trigger_protocol_violated", + "39P02": "srf_protocol_violated", + "39P03": "event_trigger_protocol_violated", + "3B000": "savepoint_exception", + "3B001": "invalid_savepoint_specification", + "3D000": "invalid_catalog_name", + "3F000": "invalid_schema_name", + "40000": "transaction_rollback", + "40002": "transaction_integrity_constraint_violation", + "40001": "serialization_failure", + "40003": "statement_completion_unknown", + "40P01": "deadlock_detected", + "42000": "syntax_error_or_access_rule_violation", + "42601": "syntax_error", + "42501": "insufficient_privilege", + "42846": "cannot_coerce", + "42803": "grouping_error", + "42P20": "windowing_error", + "42P19": "invalid_recursion", + "42830": "invalid_foreign_key", + "42602": "invalid_name", + "42622": "name_too_long", + "42939": "reserved_name", + "42804": "datatype_mismatch", + "42P18": "indeterminate_datatype", + "42P21": "collation_mismatch", + "42P22": "indeterminate_collation", + "42809": "wrong_object_type", + "428C9": "generated_always", + "42703": "undefined_column", + "42883": "undefined_function", + "42P01": "undefined_table", + "42P02": "undefined_parameter", + "42704": "undefined_object", + "42701": "duplicate_column", + "42P03": "duplicate_cursor", + "42P04": "duplicate_database", + "42723": "duplicate_function", + "42P05": "duplicate_prepared_statement", + "42P06": "duplicate_schema", + "42P07": "duplicate_table", + "42712": "duplicate_alias", + "42710": "duplicate_object", + "42702": "ambiguous_column", + "42725": "ambiguous_function", + "42P08": "ambiguous_parameter", + "42P09": "ambiguous_alias", + "42P10": "invalid_column_reference", + "42611": "invalid_column_definition", + "42P11": "invalid_cursor_definition", + "42P12": "invalid_database_definition", + "42P13": "invalid_function_definition", + "42P14": "invalid_prepared_statement_definition", + "42P15": "invalid_schema_definition", + "42P16": "invalid_table_definition", + "42P17": "invalid_object_definition", + "44000": "with_check_option_violation", + "53000": "insufficient_resources", + "53100": "disk_full", + "53200": "out_of_memory", + "53300": "too_many_connections", + "53400": "configuration_limit_exceeded", + "54000": "program_limit_exceeded", + "54001": "statement_too_complex", + "54011": "too_many_columns", + "54023": "too_many_arguments", + "55000": "object_not_in_prerequisite_state", + "55006": "object_in_use", + "55P02": "cant_change_runtime_param", + "55P03": "lock_not_available", + "55P04": "unsafe_new_enum_value_usage", + "57000": "operator_intervention", + "57014": "query_canceled", + "57P01": "admin_shutdown", + "57P02": "crash_shutdown", + "57P03": "cannot_connect_now", + "57P04": "database_dropped", + "57P05": "idle_session_timeout", + "58000": "system_error", + "58030": "io_error", + "58P01": "undefined_file", + "58P02": "duplicate_file", + "58P03": "file_name_too_long", + "F0000": "config_file_error", + "F0001": "lock_file_exists", + "HV000": "fdw_error", + "HV005": "fdw_column_name_not_found", + "HV002": "fdw_dynamic_parameter_value_needed", + "HV010": "fdw_function_sequence_error", + "HV021": "fdw_inconsistent_descriptor_information", + "HV024": "fdw_invalid_attribute_value", + "HV007": "fdw_invalid_column_name", + "HV008": "fdw_invalid_column_number", + "HV004": "fdw_invalid_data_type", + "HV006": "fdw_invalid_data_type_descriptors", + "HV091": "fdw_invalid_descriptor_field_identifier", + "HV00B": "fdw_invalid_handle", + "HV00C": "fdw_invalid_option_index", + "HV00D": "fdw_invalid_option_name", + "HV090": "fdw_invalid_string_length_or_buffer_length", + "HV00A": "fdw_invalid_string_format", + "HV009": "fdw_invalid_use_of_null_pointer", + "HV014": "fdw_too_many_handles", + "HV001": "fdw_out_of_memory", + "HV00P": "fdw_no_schemas", + "HV00J": "fdw_option_name_not_found", + "HV00K": "fdw_reply_handle", + "HV00Q": "fdw_schema_not_found", + "HV00R": "fdw_table_not_found", + "HV00L": "fdw_unable_to_create_execution", + "HV00M": "fdw_unable_to_create_reply", + "HV00N": "fdw_unable_to_establish_connection", + "P0000": "plpgsql_error", + "P0001": "raise_exception", + "P0002": "no_data_found", + "P0003": "too_many_rows", + "P0004": "assert_failure", + "XX000": "internal_error", + "XX001": "data_corrupted", + "XX002": "index_corrupted", +} diff --git a/pqerror/deprecated.go b/pqerror/deprecated.go new file mode 100644 index 000000000..d678caa03 --- /dev/null +++ b/pqerror/deprecated.go @@ -0,0 +1,44 @@ +package pqerror + +// Get implements the legacy PGError interface. +// +// Deprecated: new code should use the fields of the Error struct directly. +func (e *Error) Get(k byte) (v string) { + switch k { + case 'S': + return e.Severity + case 'C': + return string(e.Code) + case 'M': + return e.Message + case 'D': + return e.Detail + case 'H': + return e.Hint + case 'P': + return e.Position + case 'p': + return e.InternalPosition + case 'q': + return e.InternalQuery + case 'W': + return e.Where + case 's': + return e.Schema + case 't': + return e.Table + case 'c': + return e.Column + case 'd': + return e.DataTypeName + case 'n': + return e.Constraint + case 'F': + return e.File + case 'L': + return e.Line + case 'R': + return e.Routine + } + return "" +} diff --git a/pqerror/example_test.go b/pqerror/example_test.go new file mode 100644 index 000000000..e74bc3291 --- /dev/null +++ b/pqerror/example_test.go @@ -0,0 +1,25 @@ +package pqerror_test + +import ( + "database/sql" + "log" + + "github.com/lib/pq/pqerror" +) + +func ExampleAs() { + db, err := sql.Open("postgres", "") + if err != nil { + log.Fatal(err) + } + + email := "hello@example.com" + + _, err = db.Exec("insert into t (email) values ($1)", email) + if pqErr := pqerror.As(err, pqerror.UniqueViolation); pqErr != nil { + log.Fatalf("email %q already exsts", email) + } + if err != nil { + log.Fatalf("unknown error: %s", err) + } +} diff --git a/pqerror/gen.go b/pqerror/gen.go new file mode 100644 index 000000000..47c8c3c28 --- /dev/null +++ b/pqerror/gen.go @@ -0,0 +1,146 @@ +//go:build ignore + +package main + +import ( + "bufio" + "bytes" + "fmt" + "go/format" + "os" + "regexp" + "strings" +) + +func version(src string) string { + fp, err := os.Open(src + "/meson.build") + if err != nil { + panic(err) + } + defer fp.Close() + + var ( + // egrep "^\s+version: ['\"][0-9.]+['\"],?$" meson.build | egrep -o '[0-9.]+' + reLine = regexp.MustCompile(`^\s+version: ['"][0-9.]+['"],?$`) + reNum = regexp.MustCompile(`[0-9.]+`) + scan = bufio.NewScanner(fp) + ) + for scan.Scan() { + line := scan.Text() + if reLine.MatchString(line) { + return reNum.FindString(line) + } + } + if scan.Err() != nil { + panic(scan.Err()) + } + panic("version not found in meson.build") +} + +func main() { + // TODO: don't hard-code this. Or fetch from git or something. + src := os.ExpandEnv("$HOME/src/postgresql") + + v := version(src) + + fp, err := os.Open(src + "/src/backend/utils/errcodes.txt") + if err != nil { + panic(err) + } + defer fp.Close() + + var ( + scan = bufio.NewScanner(fp) + re = regexp.MustCompile(`_[^_]+`) + out = new(bytes.Buffer) + codes = new(bytes.Buffer) + names = new(bytes.Buffer) + section string + ) + fmt.Fprintf(out, "// Code generated by gen.go. DO NOT EDIT.\n\n// Last updated for PostgreSQL %s\n\n", v) + out.WriteString("package pqerror\n\nvar (\n") + for scan.Scan() { + line := scan.Text() + if line == "" || line[0] == '#' { + continue + } + // Section: Class 01 - Warning + // Section: Class 08 - Connection Exception + if strings.HasPrefix(line, "Section: ") { + section = line[9:] + //fmt.Fprintf(names, "// %s\n", section) + continue + } + + // sqlstate E/W/S errcode_macro_name spec_name (optional) + // 01000 W ERRCODE_WARNING warning + // 22022 E ERRCODE_INDICATOR_OVERFLOW indicator_overflow + // 2202E E ERRCODE_ARRAY_ELEMENT_ERROR + f := strings.Fields(line) + if len(f) == 3 { + continue + } + if len(f) != 4 { + panic(fmt.Sprintf("invalid line: %q", line)) + } + sqlstate, typ, name, spec := f[0], f[1], f[2], f[3] + _ = typ // Not used atm + + name = strings.ToLower(name) + name = strings.Replace(name, "errcode_", "_", 1) + name = re.ReplaceAllStringFunc(name, func(m string) string { + switch m { + case "_xml", "_json", "_sql", "_fdw", "_io": + return strings.ToUpper(m[1:]) + case "_sqlclient": + return "SQLClient" + case "_sqlserver": + return "SQLServer" + case "_sqlconnection": + return "SQLConnection" + case "_sqlstate": + return "SQLSTATE" + case "_plpgsql": + return "PLpgSQL" + } + return string(m[1]^0x20) + m[2:] + }) + + class := strings.HasSuffix(sqlstate, "000") + + if class { + fmt.Fprintf(out, "\tClass%s = ErrorClass(%q) // %s\n", name, sqlstate[:2], section[11:]) + } + + fmt.Fprintf(names, "\t%q: %q,\n", sqlstate, spec) + + fmt.Fprintf(codes, "\t%s = ErrorCode(%q)", name, sqlstate) + if class { + fmt.Fprintf(codes, " // %s", section) + } + fmt.Fprintln(codes) + } + if scan.Err() != nil { + panic(scan.Err()) + } + out.WriteString(")\n") + + out.WriteString("// A list of all error codes used in PostgreSQL.\n") + out.WriteString("var (\n") + out.Write(codes.Bytes()) + out.WriteString(")\n\n") + + out.WriteString("var errorCodeNames = map[ErrorCode]string{\n") + out.Write(names.Bytes()) + out.WriteString("}\n") + + gosrc, err := format.Source(out.Bytes()) + if err != nil { + panic(err) + } + + err = os.WriteFile("codes.go", gosrc, 0o777) + if err != nil { + panic(err) + } +} diff --git a/pqerror/pqerror.go b/pqerror/pqerror.go new file mode 100644 index 000000000..f3d92df61 --- /dev/null +++ b/pqerror/pqerror.go @@ -0,0 +1,247 @@ +//go:generate go run gen.go + +package pqerror + +import ( + "fmt" + "strconv" + "strings" + "unicode/utf8" +) + +// [pqerror.Error.Severity] values. +const ( + SeverityFatal = "FATAL" + SeverityPanic = "PANIC" + SeverityWarning = "WARNING" + SeverityNotice = "NOTICE" + SeverityDebug = "DEBUG" + SeverityInfo = "INFO" + SeverityLog = "LOG" +) + +type Error struct { + // [Efatal], [Epanic], [Ewarning], [Enotice], [Edebug], [Einfo], or [Elog]. + // Always present. + Severity string + + // SQLSTATE code. Always present. + Code ErrorCode + + // Primary human-readable error message. This should be accurate but terse + // (typically one line). Always present. + Message string + + // Optional secondary error message carrying more detail about the problem. + // Might run to multiple lines. + Detail string + + // Optional suggestion what to do about the problem. This is intended to + // differ from Detail in that it offers advice (potentially inappropriate) + // rather than hard facts. Might run to multiple lines. + Hint string + + // error position as an index into the original query string, as decimal + // ASCII integer. The first character has index 1, and positions are + // measured in characters not bytes. + Position string + + // This is defined the same as the Position field, but it is used when the + // cursor position refers to an internally generated command rather than the + // one submitted by the client. The InternalQuery field will always appear + // when this field appears. + InternalPosition string + + // Text of a failed internally-generated command. This could be, for + // example, an SQL query issued by a PL/pgSQL function. + InternalQuery string + + // An indication of the context in which the error occurred. Presently this + // includes a call stack traceback of active procedural language functions + // and internally-generated queries. The trace is one entry per line, most + // recent first. + Where string + + // If the error was associated with a specific database object, the name of + // the schema containing that object, if any. + Schema string + + // If the error was associated with a specific table, the name of the table. + // (Refer to the schema name field for the name of the table's schema.) + Table string + + // If the error was associated with a specific table column, the name of the + // column. (Refer to the schema and table name fields to identify the + // table.) + Column string + + // If the error was associated with a specific data type, the name of the + // data type. (Refer to the schema name field for the name of the data + // type's schema.) + DataTypeName string + + // If the error was associated with a specific constraint, the name of the + // constraint. Refer to fields listed above for the associated table or + // domain. (For this purpose, indexes are treated as constraints, even if + // they weren't created with constraint syntax.) + Constraint string + + // File name of the source-code location where the error was reported. + File string + + // Line number of the source-code location where the error was reported. + Line string + + // Name of the source-code routine reporting the error. + Routine string + + // Query is the original query. This is set by pq, and not PostgreSQL. + Query string +} + +type ( + // ErrorCode is a five-character error code. + ErrorCode string + + // ErrorClass is only the class part of an error code. + ErrorClass string +) + +// Name returns a more human friendly rendering of the error code, namely the +// "condition name". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Name() string { return errorCodeNames[ec] } + +// Class returns the error class, e.g. "28". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Class() ErrorClass { return ErrorClass(ec[:2]) } + +// Name returns the condition name of an error class. It is equivalent to the +// condition name of the "standard" error code (i.e. the one having the last +// three characters "000"). +func (ec ErrorClass) Name() string { return errorCodeNames[ErrorCode(ec+"000")] } + +// Fatal returns true if the Error Severity is fatal. +func (e *Error) Fatal() bool { return e.Severity == SeverityFatal } + +// SQLState returns the SQLState of the error. +func (e *Error) SQLState() string { return string(e.Code) } + +func (e *Error) Error() string { + msg := e.Message + if e.Query != "" && e.Position != "" { + pos, err := strconv.Atoi(e.Position) + if err == nil { + lines := strings.Split(e.Query, "\n") + line, col := posToLine(pos, lines) + if len(lines) == 1 { + msg += " at column " + strconv.Itoa(col) + } else { + msg += " at position " + strconv.Itoa(line) + ":" + strconv.Itoa(col) + } + } + } + + if e.Code != "" { + return "pq: " + msg + " (" + string(e.Code) + ")" + } + return "pq: " + msg +} + +// ErrorWithDetail returns the error message with detailed information and +// location context (if any). +// +// See the documentation on [Error]. +func (e *Error) ErrorWithDetail() string { + b := new(strings.Builder) + b.Grow(len(e.Message) + len(e.Detail) + len(e.Hint) + 30) + b.WriteString("ERROR: ") + b.WriteString(e.Message) + if e.Code != "" { + b.WriteString(" (") + b.WriteString(string(e.Code)) + b.WriteByte(')') + } + if e.Detail != "" { + b.WriteString("\nDETAIL: ") + b.WriteString(e.Detail) + } + if e.Hint != "" { + b.WriteString("\nHINT: ") + b.WriteString(e.Hint) + } + + if e.Query != "" && e.Position != "" { + b.Grow(512) + pos, err := strconv.Atoi(e.Position) + if err != nil { + return b.String() + } + lines := strings.Split(e.Query, "\n") + line, col := posToLine(pos, lines) + + fmt.Fprintf(b, "\nCONTEXT: line %d, column %d:\n\n", line, col) + if line > 2 { + fmt.Fprintf(b, "% 7d | %s\n", line-2, expandTab(lines[line-3])) + } + if line > 1 { + fmt.Fprintf(b, "% 7d | %s\n", line-1, expandTab(lines[line-2])) + } + /// Expand tabs, so that the ^ is at at the correct position, but leave + /// "column 10-13" intact. Adjusting this to the visual column would be + /// better, but we don't know the tabsize of the user in their editor, + /// which can be 8, 4, 2, or something else. We can't know. So leaving + /// it as the character index is probably the "most correct". + expanded := expandTab(lines[line-1]) + diff := len(expanded) - len(lines[line-1]) + fmt.Fprintf(b, "% 7d | %s\n", line, expanded) + fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col-1+diff), "^") + } + + return b.String() +} + +func posToLine(pos int, lines []string) (line, col int) { + read := 0 + for i := range lines { + line++ + ll := utf8.RuneCountInString(lines[i]) + 1 // +1 for the removed newline + if read+ll >= pos { + col = max(pos-read, 1) // Should be lower than 1, but just in case. + break + } + read += ll + } + return line, col +} + +func expandTab(s string) string { + var ( + b strings.Builder + l int + fill = func(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = ' ' + } + return string(b) + } + ) + b.Grow(len(s)) + for _, r := range s { + switch r { + case '\t': + tw := 8 - l%8 + b.WriteString(fill(tw)) + l += tw + default: + b.WriteRune(r) + l += 1 + } + } + return b.String() +} diff --git a/pqerror/pqerror_test.go b/pqerror/pqerror_test.go new file mode 100644 index 000000000..678e263a4 --- /dev/null +++ b/pqerror/pqerror_test.go @@ -0,0 +1,61 @@ +package pqerror + +import ( + "errors" + "os" + "reflect" + "testing" +) + +func TestAs(t *testing.T) { + tests := []struct { + err error + codes []ErrorCode + wantReturn bool + }{ + {nil, nil, false}, + {nil, []ErrorCode{SyntaxError}, false}, + {errors.New("oh noes"), []ErrorCode{SyntaxError}, false}, + {&Error{Code: "00000", Message: "okay"}, nil, true}, + + {&Error{Code: "00000", Message: "okay"}, []ErrorCode{SyntaxError}, false}, + {&Error{Code: "00000", Message: "okay"}, []ErrorCode{SyntaxError, SuccessfulCompletion}, true}, + } + + //t.Parallel() + for _, tt := range tests { + t.Run("", func(t *testing.T) { + have := As(tt.err, tt.codes...) + if tt.wantReturn { + if !reflect.DeepEqual(have, tt.err) { + t.Errorf("\nhave: %#v\nwant: %#v", have, tt.err) + } + } else { + if have != nil { + t.Errorf("expected return to be nil, but have:\n%#v", have) + } + } + }) + } +} + +func BenchmarkAs(b *testing.B) { + b.Run("nil", func(b *testing.B) { + var nilerr error + for i := 0; i < b.N; i++ { + _ = As(nilerr, SuccessfulCompletion) + } + }) + b.Run("other error", func(b *testing.B) { + patherr := &os.PathError{} + for i := 0; i < b.N; i++ { + _ = As(patherr, SuccessfulCompletion) + } + }) + b.Run("pq.Error", func(b *testing.B) { + pqerr := &Error{Code: "00000", Message: "okay"} + for i := 0; i < b.N; i++ { + _ = As(pqerr, SuccessfulCompletion) + } + }) +} From 67e93c949f6033fa423e6374cf7891529a13da33 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 17 Mar 2026 21:35:28 +0000 Subject: [PATCH 2/3] Don't move pq.Error to pqerror.Error Still move pq.ErrorCode and pq.ErrorClass to pqerror.Code and pqerror.Class, so we can use it in pqerror. Also move the error severity values here. Seems to make sense. --- CHANGELOG.md | 8 +- README.md | 14 +- pqerror/as.go => as.go | 7 +- pqerror/as_126.go => as_go126.go | 10 +- conn_test.go | 2 +- deprecated.go | 60 +++ error.go | 243 ++++++++++-- error_test.go | 135 +++++-- example_test.go | 18 + helper_test.go | 3 +- pqerror/codes.go | 610 +++++++++++++++---------------- pqerror/deprecated.go | 44 --- pqerror/example_test.go | 25 -- pqerror/gen.go | 6 +- pqerror/pqerror.go | 252 +------------ pqerror/pqerror_test.go | 61 ---- 16 files changed, 749 insertions(+), 749 deletions(-) rename pqerror/as.go => as.go (68%) rename pqerror/as_126.go => as_go126.go (51%) delete mode 100644 pqerror/deprecated.go delete mode 100644 pqerror/example_test.go delete mode 100644 pqerror/pqerror_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 95a3949fa..476c89994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,14 +32,13 @@ unreleased For example, to test if an error is a UNIQUE constraint violation: - pqErr, ok := pq.AsType[*pq.Error](err) - if ok && pqErr.Code == pqerror.UniqueViolation { + if pqErr, ok := errors.AsType[*pq.Error](err); ok && pqErr.Code == pqerror.UniqueViolation { log.Fatalf("email %q already exsts", email) } - To make this a bit more convenient, it also adds a pqerror.As() function: + To make this a bit more convenient, it also adds a `pq.As()` function: - pqErr := pqerror.As(err, pqerror.UniqueViolation) + pqErr := pq.As(err, pqerror.UniqueViolation) if pqErr != nil { log.Fatalf("email %q already exsts", email) } @@ -74,7 +73,6 @@ unreleased [#1283]: https://github.com/lib/pq/pull/1283 [#1285]: https://github.com/lib/pq/pull/1285 - v1.11.2 (2026-02-10) -------------------- This fixes two regressions: diff --git a/README.md b/README.md index 5aedfa6c3..5ca523d42 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,17 @@ The libpq way (which also works in pq) is to use `options='-c k=v'` like so: Errors ------ -Errors from PostgreSQL are returned as [pq.Error]; the Error() string contains -the error message and code: +Errors from PostgreSQL are returned as [pq.Error]; [pq.As] can be used to +convert an error to `pq.Error`: + +```go +pqErr := pq.As(err, pqerror.UniqueViolation) +if pqErr != nil { + return fmt.Errorf("email %q already exsts", email) +} +``` + +the Error() string contains the error message and code: pq: duplicate key value violates unique constraint "users_lower_idx" (23505) @@ -118,6 +127,7 @@ It contains the context where this error occurred: ^ [pq.Error]: https://pkg.go.dev/github.com/lib/pq#Error +[pq.As]: https://pkg.go.dev/github.com/lib/pq#As PostgreSQL features ------------------- diff --git a/pqerror/as.go b/as.go similarity index 68% rename from pqerror/as.go rename to as.go index f8d918d50..1ea0ee5bb 100644 --- a/pqerror/as.go +++ b/as.go @@ -1,15 +1,16 @@ //go:build !go1.26 -package pqerror +package pq import ( "errors" "slices" ) -// As asserts that the given error is [pqerror.Error] and returns it. +// As asserts that the given error is [pq.Error] and returns it, returning nil +// if it's not a pq.Error. // -// It will return nil if the Error is not one of the given error codes. If no +// It will return nil if the pq.Error is not one of the given error codes. If no // codes are given it will always return the Error. // // This is safe to call with a nil error. diff --git a/pqerror/as_126.go b/as_go126.go similarity index 51% rename from pqerror/as_126.go rename to as_go126.go index eb15545df..18ffbc375 100644 --- a/pqerror/as_126.go +++ b/as_go126.go @@ -1,19 +1,21 @@ //go:build go1.26 -package pqerror +package pq import ( "errors" + "github.com/lib/pq/pqerror" "slices" ) -// As asserts that the given error is [pqerror.Error] and returns it. +// As asserts that the given error is [pq.Error] and returns it, returning nil +// if it's not a pq.Error. // -// It will return nil if the Error is not one of the given error codes. If no +// It will return nil if the pq.Error is not one of the given error codes. If no // codes are given it will always return the Error. // // This is safe to call with a nil error. -func As(err error, codes ...ErrorCode) *Error { +func As(err error, codes ...pqerror.Code) *Error { if pqErr, ok := errors.AsType[*Error](err); ok && (len(codes) == 0 || slices.Contains(codes, pqErr.Code)) { return pqErr } diff --git a/conn_test.go b/conn_test.go index 2f7e004b7..2883b28d5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -642,7 +642,7 @@ func TestErrorDuringStartupClosesConn(t *testing.T) { func TestBadConn(t *testing.T) { t.Parallel() - for _, tt := range []error{io.EOF, &Error{Severity: Efatal}} { + for _, tt := range []error{io.EOF, &Error{Severity: pqerror.SeverityFatal}} { t.Run(fmt.Sprintf("%s", tt), func(t *testing.T) { var cn conn err := cn.handleError(tt) diff --git a/deprecated.go b/deprecated.go index 8ab257cba..d43934a0a 100644 --- a/deprecated.go +++ b/deprecated.go @@ -3,6 +3,23 @@ package pq import ( "bytes" "database/sql" + + "github.com/lib/pq/pqerror" +) + +// [pq.Error.Severity] values. +// +// Deprecated: use pqerror.Severity[..] values. +// +//go:fix inline +const ( + Efatal = pqerror.SeverityFatal + Epanic = pqerror.SeverityPanic + Ewarning = pqerror.SeverityWarning + Enotice = pqerror.SeverityNotice + Edebug = pqerror.SeverityDebug + Einfo = pqerror.SeverityInfo + Elog = pqerror.SeverityLog ) // PGError is an interface used by previous versions of pq. @@ -14,6 +31,49 @@ type PGError interface { Get(k byte) (v string) } +// Get implements the legacy PGError interface. +// +// Deprecated: new code should use the fields of the Error struct directly. +func (e *Error) Get(k byte) (v string) { + switch k { + case 'S': + return e.Severity + case 'C': + return string(e.Code) + case 'M': + return e.Message + case 'D': + return e.Detail + case 'H': + return e.Hint + case 'P': + return e.Position + case 'p': + return e.InternalPosition + case 'q': + return e.InternalQuery + case 'W': + return e.Where + case 's': + return e.Schema + case 't': + return e.Table + case 'c': + return e.Column + case 'd': + return e.DataTypeName + case 'n': + return e.Constraint + case 'F': + return e.File + case 'L': + return e.Line + case 'R': + return e.Routine + } + return "" +} + // ParseURL converts a url to a connection string for driver.Open. // // Deprecated: directly passing an URL to sql.Open("postgres", "postgres://...") diff --git a/error.go b/error.go index b6a644c4d..0851a66be 100644 --- a/error.go +++ b/error.go @@ -6,24 +6,16 @@ import ( "io" "net" "runtime" + "strconv" + "strings" + "unicode/utf8" "github.com/lib/pq/pqerror" ) -// [pq.Error.Severity] values. -const ( - Efatal = pqerror.SeverityFatal - Epanic = pqerror.SeverityPanic - Ewarning = pqerror.SeverityWarning - Enotice = pqerror.SeverityNotice - Edebug = pqerror.SeverityDebug - Einfo = pqerror.SeverityInfo - Elog = pqerror.SeverityLog -) - -// Error represents an error communicating with the server. +// Error returned by the PostgreSQL server. // -// The [Error] method only returns the error message and error code: +// The [Error] method returns the error message and error code: // // pq: invalid input syntax for type json (22P02) // @@ -38,25 +30,109 @@ const ( // 4 | 123, // 5 | 'foo', 'asd'::jsonb // ^ -// -// See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields -type Error = pqerror.Error +type Error struct { + // [Efatal], [Epanic], [Ewarning], [Enotice], [Edebug], [Einfo], or [Elog]. + // Always present. + Severity string + + // SQLSTATE code. Always present. + Code pqerror.Code + + // Primary human-readable error message. This should be accurate but terse + // (typically one line). Always present. + Message string + + // Optional secondary error message carrying more detail about the problem. + // Might run to multiple lines. + Detail string + + // Optional suggestion what to do about the problem. This is intended to + // differ from Detail in that it offers advice (potentially inappropriate) + // rather than hard facts. Might run to multiple lines. + Hint string + + // error position as an index into the original query string, as decimal + // ASCII integer. The first character has index 1, and positions are + // measured in characters not bytes. + Position string + + // This is defined the same as the Position field, but it is used when the + // cursor position refers to an internally generated command rather than the + // one submitted by the client. The InternalQuery field will always appear + // when this field appears. + InternalPosition string + + // Text of a failed internally-generated command. This could be, for + // example, an SQL query issued by a PL/pgSQL function. + InternalQuery string + + // An indication of the context in which the error occurred. Presently this + // includes a call stack traceback of active procedural language functions + // and internally-generated queries. The trace is one entry per line, most + // recent first. + Where string + + // If the error was associated with a specific database object, the name of + // the schema containing that object, if any. + Schema string + + // If the error was associated with a specific table, the name of the table. + // (Refer to the schema name field for the name of the table's schema.) + Table string -// ErrorCode is a five-character error code. -type ErrorCode = pqerror.ErrorCode + // If the error was associated with a specific table column, the name of the + // column. (Refer to the schema and table name fields to identify the + // table.) + Column string -// ErrorClass is only the class part of an error code. -type ErrorClass = pqerror.ErrorClass + // If the error was associated with a specific data type, the name of the + // data type. (Refer to the schema name field for the name of the data + // type's schema.) + DataTypeName string + + // If the error was associated with a specific constraint, the name of the + // constraint. Refer to fields listed above for the associated table or + // domain. (For this purpose, indexes are treated as constraints, even if + // they weren't created with constraint syntax.) + Constraint string + + // File name of the source-code location where the error was reported. + File string + + // Line number of the source-code location where the error was reported. + Line string + + // Name of the source-code routine reporting the error. + Routine string + + query string +} + +type ( + // ErrorCode is a five-character error code. + // + // Deprecated: use pqerror.Code + // + //go:fix inline + ErrorCode = pqerror.Code + + // ErrorClass is only the class part of an error code. + // + // Deprecated: use pqerror.Class + // + //go:fix inline + ErrorClass = pqerror.Class +) func parseError(r *readBuf, q string) *Error { - err := &Error{Query: q} + err := &Error{query: q} for t := r.byte(); t != 0; t = r.byte() { msg := r.string() switch t { case 'S': err.Severity = msg case 'C': - err.Code = ErrorCode(msg) + err.Code = pqerror.Code(msg) case 'M': err.Message = msg case 'D': @@ -92,6 +168,127 @@ func parseError(r *readBuf, q string) *Error { return err } +// Fatal returns true if the Error Severity is fatal. +func (e *Error) Fatal() bool { return e.Severity == pqerror.SeverityFatal } + +// SQLState returns the SQLState of the error. +func (e *Error) SQLState() string { return string(e.Code) } + +func (e *Error) Error() string { + msg := e.Message + if e.query != "" && e.Position != "" { + pos, err := strconv.Atoi(e.Position) + if err == nil { + lines := strings.Split(e.query, "\n") + line, col := posToLine(pos, lines) + if len(lines) == 1 { + msg += " at column " + strconv.Itoa(col) + } else { + msg += " at position " + strconv.Itoa(line) + ":" + strconv.Itoa(col) + } + } + } + + if e.Code != "" { + return "pq: " + msg + " (" + string(e.Code) + ")" + } + return "pq: " + msg +} + +// ErrorWithDetail returns the error message with detailed information and +// location context (if any). +// +// See the documentation on [Error]. +func (e *Error) ErrorWithDetail() string { + b := new(strings.Builder) + b.Grow(len(e.Message) + len(e.Detail) + len(e.Hint) + 30) + b.WriteString("ERROR: ") + b.WriteString(e.Message) + if e.Code != "" { + b.WriteString(" (") + b.WriteString(string(e.Code)) + b.WriteByte(')') + } + if e.Detail != "" { + b.WriteString("\nDETAIL: ") + b.WriteString(e.Detail) + } + if e.Hint != "" { + b.WriteString("\nHINT: ") + b.WriteString(e.Hint) + } + + if e.query != "" && e.Position != "" { + b.Grow(512) + pos, err := strconv.Atoi(e.Position) + if err != nil { + return b.String() + } + lines := strings.Split(e.query, "\n") + line, col := posToLine(pos, lines) + + fmt.Fprintf(b, "\nCONTEXT: line %d, column %d:\n\n", line, col) + if line > 2 { + fmt.Fprintf(b, "% 7d | %s\n", line-2, expandTab(lines[line-3])) + } + if line > 1 { + fmt.Fprintf(b, "% 7d | %s\n", line-1, expandTab(lines[line-2])) + } + /// Expand tabs, so that the ^ is at at the correct position, but leave + /// "column 10-13" intact. Adjusting this to the visual column would be + /// better, but we don't know the tabsize of the user in their editor, + /// which can be 8, 4, 2, or something else. We can't know. So leaving + /// it as the character index is probably the "most correct". + expanded := expandTab(lines[line-1]) + diff := len(expanded) - len(lines[line-1]) + fmt.Fprintf(b, "% 7d | %s\n", line, expanded) + fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col-1+diff), "^") + } + + return b.String() +} + +func posToLine(pos int, lines []string) (line, col int) { + read := 0 + for i := range lines { + line++ + ll := utf8.RuneCountInString(lines[i]) + 1 // +1 for the removed newline + if read+ll >= pos { + col = max(pos-read, 1) // Should be lower than 1, but just in case. + break + } + read += ll + } + return line, col +} + +func expandTab(s string) string { + var ( + b strings.Builder + l int + fill = func(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = ' ' + } + return string(b) + } + ) + b.Grow(len(s)) + for _, r := range s { + switch r { + case '\t': + tw := 8 - l%8 + b.WriteString(fill(tw)) + l += tw + default: + b.WriteRune(r) + l += 1 + } + } + return b.String() +} + func (cn *conn) handleError(reported error, query ...string) error { switch err := reported.(type) { case nil: @@ -103,7 +300,7 @@ func (cn *conn) handleError(reported error, query ...string) error { reported = driver.ErrBadConn case *Error: if len(query) > 0 && query[0] != "" { - err.Query = query[0] + err.query = query[0] reported = err } if err.Fatal() { diff --git a/error_test.go b/error_test.go index 841bb346f..95dde1adf 100644 --- a/error_test.go +++ b/error_test.go @@ -7,11 +7,13 @@ import ( "fmt" "net" "os" + "reflect" "strconv" "testing" "time" "github.com/lib/pq/internal/pqtest" + "github.com/lib/pq/pqerror" ) func TestErrorSQLState(t *testing.T) { @@ -140,7 +142,7 @@ func TestError(t *testing.T) { t.Errorf("\nhave: %s\nwant: %s", err.Error(), tt.want) } tt.wantDetail = pqtest.NormalizeIndent(tt.wantDetail) - if pqErr.Query != "" && pqErr.Position != "" { + if pqErr.query != "" && pqErr.Position != "" { tt.wantDetail += "\n" } if pqErr.ErrorWithDetail() != tt.wantDetail { @@ -150,45 +152,6 @@ func TestError(t *testing.T) { } } -func BenchmarkError(b *testing.B) { - db := pqtest.MustDB(b) - _, err := db.Exec(pqtest.NormalizeIndent(` - create table browsers ( - browser_id serial, - name varchar, - version varchar - ); - create unique index "browsers#name#version" on browsers(name, version); - - create table systems ( - system_id serial, - name varchar, - version varchar, - ); - create unique index "systems#name#version" on systems(name, version); - `)) - if err == nil { - b.Fatal("err is nil?") - } - - b.ResetTimer() - b.Run("error", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = err.Error() - } - }) - b.Run("errorWithDetail", func(b *testing.B) { - pqErr := new(Error) - if !errors.As(err, &pqErr) { - b.Fatalf("not pq.Error: %T", err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = pqErr.ErrorWithDetail() - } - }) -} - type ( failConn struct{ net.Conn } failDialer struct{ d net.Dialer } @@ -298,3 +261,95 @@ func TestNetworkError(t *testing.T) { t.Fatal(err) } } + +func TestAs(t *testing.T) { + tests := []struct { + err error + codes []pqerror.Code + wantReturn bool + }{ + {nil, nil, false}, + {nil, []pqerror.Code{pqerror.SyntaxError}, false}, + {errors.New("oh noes"), []pqerror.Code{pqerror.SyntaxError}, false}, + {&Error{Code: "00000", Message: "okay"}, nil, true}, + + {&Error{Code: "00000", Message: "okay"}, []pqerror.Code{pqerror.SyntaxError}, false}, + {&Error{Code: "00000", Message: "okay"}, []pqerror.Code{pqerror.SyntaxError, pqerror.SuccessfulCompletion}, true}, + } + + t.Parallel() + for _, tt := range tests { + t.Run("", func(t *testing.T) { + have := As(tt.err, tt.codes...) + if tt.wantReturn { + if !reflect.DeepEqual(have, tt.err) { + t.Errorf("\nhave: %#v\nwant: %#v", have, tt.err) + } + } else { + if have != nil { + t.Errorf("expected return to be nil, but have:\n%#v", have) + } + } + }) + } +} + +func BenchmarkError(b *testing.B) { + db := pqtest.MustDB(b) + _, err := db.Exec(pqtest.NormalizeIndent(` + create table browsers ( + browser_id serial, + name varchar, + version varchar + ); + create unique index "browsers#name#version" on browsers(name, version); + + create table systems ( + system_id serial, + name varchar, + version varchar, + ); + create unique index "systems#name#version" on systems(name, version); + `)) + if err == nil { + b.Fatal("err is nil?") + } + + b.ResetTimer() + b.Run("error", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = err.Error() + } + }) + b.Run("errorWithDetail", func(b *testing.B) { + pqErr := new(Error) + if !errors.As(err, &pqErr) { + b.Fatalf("not pq.Error: %T", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pqErr.ErrorWithDetail() + } + }) +} + +func BenchmarkAs(b *testing.B) { + b.Run("nil", func(b *testing.B) { + var nilerr error + for i := 0; i < b.N; i++ { + _ = As(nilerr, pqerror.SuccessfulCompletion) + } + }) + b.Run("other error", func(b *testing.B) { + patherr := &os.PathError{} + for i := 0; i < b.N; i++ { + _ = As(patherr, pqerror.SuccessfulCompletion) + } + }) + b.Run("pq.Error", func(b *testing.B) { + pqerr := &Error{Code: "00000", Message: "okay"} + for i := 0; i < b.N; i++ { + _ = As(pqerr, pqerror.SuccessfulCompletion) + } + }) +} diff --git a/example_test.go b/example_test.go index 19511feac..0cc98011b 100644 --- a/example_test.go +++ b/example_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/lib/pq" + "github.com/lib/pq/pqerror" ) func Example_open() { @@ -339,3 +340,20 @@ func ExampleListener() { // notification on "banana" with data "yellow and curvy" // nil notify: closing Listener } + +func ExampleAs() { + db, err := sql.Open("postgres", "") + if err != nil { + log.Fatal(err) + } + + email := "hello@example.com" + + _, err = db.Exec("insert into t (email) values ($1)", email) + if pqErr := pq.As(err, pqerror.UniqueViolation); pqErr != nil { + log.Fatalf("email %q already exsts", email) + } + if err != nil { + log.Fatalf("unknown error: %s", err) + } +} diff --git a/helper_test.go b/helper_test.go index e5733a81b..f2224ca2b 100644 --- a/helper_test.go +++ b/helper_test.go @@ -4,12 +4,13 @@ import ( "testing" "github.com/lib/pq/internal/pqtest" + "github.com/lib/pq/pqerror" ) // Called for the side-effect of setting the environment. func init() { pqtest.DSN("") } -const cancelErrorCode ErrorCode = "57014" +const cancelErrorCode pqerror.Code = "57014" // pqError converts an error to *pq.Error, calling t.Fatal() if the error is nil // or if this fails. diff --git a/pqerror/codes.go b/pqerror/codes.go index 9c6781b3d..f55766440 100644 --- a/pqerror/codes.go +++ b/pqerror/codes.go @@ -5,317 +5,317 @@ package pqerror var ( - ClassSuccessfulCompletion = ErrorClass("00") // Successful Completion - ClassWarning = ErrorClass("01") // Warning - ClassNoData = ErrorClass("02") // No Data (this is also a warning class per the SQL standard) - ClassSQLStatementNotYetComplete = ErrorClass("03") // SQL Statement Not Yet Complete - ClassConnectionException = ErrorClass("08") // Connection Exception - ClassTriggeredActionException = ErrorClass("09") // Triggered Action Exception - ClassFeatureNotSupported = ErrorClass("0A") // Feature Not Supported - ClassInvalidTransactionInitiation = ErrorClass("0B") // Invalid Transaction Initiation - ClassLocatorException = ErrorClass("0F") // Locator Exception - ClassInvalidGrantor = ErrorClass("0L") // Invalid Grantor - ClassInvalidRoleSpecification = ErrorClass("0P") // Invalid Role Specification - ClassDiagnosticsException = ErrorClass("0Z") // Diagnostics Exception - ClassCaseNotFound = ErrorClass("20") // Case Not Found - ClassCardinalityViolation = ErrorClass("21") // Cardinality Violation - ClassDataException = ErrorClass("22") // Data Exception - ClassIntegrityConstraintViolation = ErrorClass("23") // Integrity Constraint Violation - ClassInvalidCursorState = ErrorClass("24") // Invalid Cursor State - ClassInvalidTransactionState = ErrorClass("25") // Invalid Transaction State - ClassInvalidSQLStatementName = ErrorClass("26") // Invalid SQL Statement Name - ClassTriggeredDataChangeViolation = ErrorClass("27") // Triggered Data Change Violation - ClassInvalidAuthorizationSpecification = ErrorClass("28") // Invalid Authorization Specification - ClassDependentPrivilegeDescriptorsStillExist = ErrorClass("2B") // Dependent Privilege Descriptors Still Exist - ClassInvalidTransactionTermination = ErrorClass("2D") // Invalid Transaction Termination - ClassSQLRoutineException = ErrorClass("2F") // SQL Routine Exception - ClassInvalidCursorName = ErrorClass("34") // Invalid Cursor Name - ClassExternalRoutineException = ErrorClass("38") // External Routine Exception - ClassExternalRoutineInvocationException = ErrorClass("39") // External Routine Invocation Exception - ClassSavepointException = ErrorClass("3B") // Savepoint Exception - ClassInvalidCatalogName = ErrorClass("3D") // Invalid Catalog Name - ClassInvalidSchemaName = ErrorClass("3F") // Invalid Schema Name - ClassTransactionRollback = ErrorClass("40") // Transaction Rollback - ClassSyntaxErrorOrAccessRuleViolation = ErrorClass("42") // Syntax Error or Access Rule Violation - ClassWithCheckOptionViolation = ErrorClass("44") // WITH CHECK OPTION Violation - ClassInsufficientResources = ErrorClass("53") // Insufficient Resources - ClassProgramLimitExceeded = ErrorClass("54") // Program Limit Exceeded - ClassObjectNotInPrerequisiteState = ErrorClass("55") // Object Not In Prerequisite State - ClassOperatorIntervention = ErrorClass("57") // Operator Intervention - ClassSystemError = ErrorClass("58") // System Error (errors external to PostgreSQL itself) - ClassConfigFileError = ErrorClass("F0") // Configuration File Error - ClassFDWError = ErrorClass("HV") // Foreign Data Wrapper Error (SQL/MED) - ClassPLpgSQLError = ErrorClass("P0") // PL/pgSQL Error - ClassInternalError = ErrorClass("XX") // Internal Error + ClassSuccessfulCompletion = Class("00") // Successful Completion + ClassWarning = Class("01") // Warning + ClassNoData = Class("02") // No Data (this is also a warning class per the SQL standard) + ClassSQLStatementNotYetComplete = Class("03") // SQL Statement Not Yet Complete + ClassConnectionException = Class("08") // Connection Exception + ClassTriggeredActionException = Class("09") // Triggered Action Exception + ClassFeatureNotSupported = Class("0A") // Feature Not Supported + ClassInvalidTransactionInitiation = Class("0B") // Invalid Transaction Initiation + ClassLocatorException = Class("0F") // Locator Exception + ClassInvalidGrantor = Class("0L") // Invalid Grantor + ClassInvalidRoleSpecification = Class("0P") // Invalid Role Specification + ClassDiagnosticsException = Class("0Z") // Diagnostics Exception + ClassCaseNotFound = Class("20") // Case Not Found + ClassCardinalityViolation = Class("21") // Cardinality Violation + ClassDataException = Class("22") // Data Exception + ClassIntegrityConstraintViolation = Class("23") // Integrity Constraint Violation + ClassInvalidCursorState = Class("24") // Invalid Cursor State + ClassInvalidTransactionState = Class("25") // Invalid Transaction State + ClassInvalidSQLStatementName = Class("26") // Invalid SQL Statement Name + ClassTriggeredDataChangeViolation = Class("27") // Triggered Data Change Violation + ClassInvalidAuthorizationSpecification = Class("28") // Invalid Authorization Specification + ClassDependentPrivilegeDescriptorsStillExist = Class("2B") // Dependent Privilege Descriptors Still Exist + ClassInvalidTransactionTermination = Class("2D") // Invalid Transaction Termination + ClassSQLRoutineException = Class("2F") // SQL Routine Exception + ClassInvalidCursorName = Class("34") // Invalid Cursor Name + ClassExternalRoutineException = Class("38") // External Routine Exception + ClassExternalRoutineInvocationException = Class("39") // External Routine Invocation Exception + ClassSavepointException = Class("3B") // Savepoint Exception + ClassInvalidCatalogName = Class("3D") // Invalid Catalog Name + ClassInvalidSchemaName = Class("3F") // Invalid Schema Name + ClassTransactionRollback = Class("40") // Transaction Rollback + ClassSyntaxErrorOrAccessRuleViolation = Class("42") // Syntax Error or Access Rule Violation + ClassWithCheckOptionViolation = Class("44") // WITH CHECK OPTION Violation + ClassInsufficientResources = Class("53") // Insufficient Resources + ClassProgramLimitExceeded = Class("54") // Program Limit Exceeded + ClassObjectNotInPrerequisiteState = Class("55") // Object Not In Prerequisite State + ClassOperatorIntervention = Class("57") // Operator Intervention + ClassSystemError = Class("58") // System Error (errors external to PostgreSQL itself) + ClassConfigFileError = Class("F0") // Configuration File Error + ClassFDWError = Class("HV") // Foreign Data Wrapper Error (SQL/MED) + ClassPLpgSQLError = Class("P0") // PL/pgSQL Error + ClassInternalError = Class("XX") // Internal Error ) // A list of all error codes used in PostgreSQL. var ( - SuccessfulCompletion = ErrorCode("00000") // Class 00 - Successful Completion - Warning = ErrorCode("01000") // Class 01 - Warning - WarningDynamicResultSetsReturned = ErrorCode("0100C") - WarningImplicitZeroBitPadding = ErrorCode("01008") - WarningNullValueEliminatedInSetFunction = ErrorCode("01003") - WarningPrivilegeNotGranted = ErrorCode("01007") - WarningPrivilegeNotRevoked = ErrorCode("01006") - WarningStringDataRightTruncation = ErrorCode("01004") - WarningDeprecatedFeature = ErrorCode("01P01") - NoData = ErrorCode("02000") // Class 02 - No Data (this is also a warning class per the SQL standard) - NoAdditionalDynamicResultSetsReturned = ErrorCode("02001") - SQLStatementNotYetComplete = ErrorCode("03000") // Class 03 - SQL Statement Not Yet Complete - ConnectionException = ErrorCode("08000") // Class 08 - Connection Exception - ConnectionDoesNotExist = ErrorCode("08003") - ConnectionFailure = ErrorCode("08006") - SQLClientUnableToEstablishSQLConnection = ErrorCode("08001") - SQLServerRejectedEstablishmentOfSQLConnection = ErrorCode("08004") - TransactionResolutionUnknown = ErrorCode("08007") - ProtocolViolation = ErrorCode("08P01") - TriggeredActionException = ErrorCode("09000") // Class 09 - Triggered Action Exception - FeatureNotSupported = ErrorCode("0A000") // Class 0A - Feature Not Supported - InvalidTransactionInitiation = ErrorCode("0B000") // Class 0B - Invalid Transaction Initiation - LocatorException = ErrorCode("0F000") // Class 0F - Locator Exception - LEInvalidSpecification = ErrorCode("0F001") - InvalidGrantor = ErrorCode("0L000") // Class 0L - Invalid Grantor - InvalidGrantOperation = ErrorCode("0LP01") - InvalidRoleSpecification = ErrorCode("0P000") // Class 0P - Invalid Role Specification - DiagnosticsException = ErrorCode("0Z000") // Class 0Z - Diagnostics Exception - StackedDiagnosticsAccessedWithoutActiveHandler = ErrorCode("0Z002") - InvalidArgumentForXquery = ErrorCode("10608") - CaseNotFound = ErrorCode("20000") // Class 20 - Case Not Found - CardinalityViolation = ErrorCode("21000") // Class 21 - Cardinality Violation - DataException = ErrorCode("22000") // Class 22 - Data Exception - ArraySubscriptError = ErrorCode("2202E") - CharacterNotInRepertoire = ErrorCode("22021") - DatetimeFieldOverflow = ErrorCode("22008") - DivisionByZero = ErrorCode("22012") - ErrorInAssignment = ErrorCode("22005") - EscapeCharacterConflict = ErrorCode("2200B") - IndicatorOverflow = ErrorCode("22022") - IntervalFieldOverflow = ErrorCode("22015") - InvalidArgumentForLog = ErrorCode("2201E") - InvalidArgumentForNtile = ErrorCode("22014") - InvalidArgumentForNthValue = ErrorCode("22016") - InvalidArgumentForPowerFunction = ErrorCode("2201F") - InvalidArgumentForWidthBucketFunction = ErrorCode("2201G") - InvalidCharacterValueForCast = ErrorCode("22018") - InvalidDatetimeFormat = ErrorCode("22007") - InvalidEscapeCharacter = ErrorCode("22019") - InvalidEscapeOctet = ErrorCode("2200D") - InvalidEscapeSequence = ErrorCode("22025") - NonstandardUseOfEscapeCharacter = ErrorCode("22P06") - InvalidIndicatorParameterValue = ErrorCode("22010") - InvalidParameterValue = ErrorCode("22023") - InvalidPrecedingOrFollowingSize = ErrorCode("22013") - InvalidRegularExpression = ErrorCode("2201B") - InvalidRowCountInLimitClause = ErrorCode("2201W") - InvalidRowCountInResultOffsetClause = ErrorCode("2201X") - InvalidTablesampleArgument = ErrorCode("2202H") - InvalidTablesampleRepeat = ErrorCode("2202G") - InvalidTimeZoneDisplacementValue = ErrorCode("22009") - InvalidUseOfEscapeCharacter = ErrorCode("2200C") - MostSpecificTypeMismatch = ErrorCode("2200G") - NullValueNotAllowed = ErrorCode("22004") - NullValueNoIndicatorParameter = ErrorCode("22002") - NumericValueOutOfRange = ErrorCode("22003") - SequenceGeneratorLimitExceeded = ErrorCode("2200H") - StringDataLengthMismatch = ErrorCode("22026") - StringDataRightTruncation = ErrorCode("22001") - SubstringError = ErrorCode("22011") - TrimError = ErrorCode("22027") - UnterminatedCString = ErrorCode("22024") - ZeroLengthCharacterString = ErrorCode("2200F") - FloatingPointException = ErrorCode("22P01") - InvalidTextRepresentation = ErrorCode("22P02") - InvalidBinaryRepresentation = ErrorCode("22P03") - BadCopyFileFormat = ErrorCode("22P04") - UntranslatableCharacter = ErrorCode("22P05") - NotAnXMLDocument = ErrorCode("2200L") - InvalidXMLDocument = ErrorCode("2200M") - InvalidXMLContent = ErrorCode("2200N") - InvalidXMLComment = ErrorCode("2200S") - InvalidXMLProcessingInstruction = ErrorCode("2200T") - DuplicateJSONObjectKeyValue = ErrorCode("22030") - InvalidArgumentForSQLJSONDatetimeFunction = ErrorCode("22031") - InvalidJSONText = ErrorCode("22032") - InvalidSQLJSONSubscript = ErrorCode("22033") - MoreThanOneSQLJSONItem = ErrorCode("22034") - NoSQLJSONItem = ErrorCode("22035") - NonNumericSQLJSONItem = ErrorCode("22036") - NonUniqueKeysInAJSONObject = ErrorCode("22037") - SingletonSQLJSONItemRequired = ErrorCode("22038") - SQLJSONArrayNotFound = ErrorCode("22039") - SQLJSONMemberNotFound = ErrorCode("2203A") - SQLJSONNumberNotFound = ErrorCode("2203B") - SQLJSONObjectNotFound = ErrorCode("2203C") - TooManyJSONArrayElements = ErrorCode("2203D") - TooManyJSONObjectMembers = ErrorCode("2203E") - SQLJSONScalarRequired = ErrorCode("2203F") - SQLJSONItemCannotBeCastToTargetType = ErrorCode("2203G") - IntegrityConstraintViolation = ErrorCode("23000") // Class 23 - Integrity Constraint Violation - RestrictViolation = ErrorCode("23001") - NotNullViolation = ErrorCode("23502") - ForeignKeyViolation = ErrorCode("23503") - UniqueViolation = ErrorCode("23505") - CheckViolation = ErrorCode("23514") - ExclusionViolation = ErrorCode("23P01") - InvalidCursorState = ErrorCode("24000") // Class 24 - Invalid Cursor State - InvalidTransactionState = ErrorCode("25000") // Class 25 - Invalid Transaction State - ActiveSQLTransaction = ErrorCode("25001") - BranchTransactionAlreadyActive = ErrorCode("25002") - HeldCursorRequiresSameIsolationLevel = ErrorCode("25008") - InappropriateAccessModeForBranchTransaction = ErrorCode("25003") - InappropriateIsolationLevelForBranchTransaction = ErrorCode("25004") - NoActiveSQLTransactionForBranchTransaction = ErrorCode("25005") - ReadOnlySQLTransaction = ErrorCode("25006") - SchemaAndDataStatementMixingNotSupported = ErrorCode("25007") - NoActiveSQLTransaction = ErrorCode("25P01") - InFailedSQLTransaction = ErrorCode("25P02") - IdleInTransactionSessionTimeout = ErrorCode("25P03") - TransactionTimeout = ErrorCode("25P04") - InvalidSQLStatementName = ErrorCode("26000") // Class 26 - Invalid SQL Statement Name - TriggeredDataChangeViolation = ErrorCode("27000") // Class 27 - Triggered Data Change Violation - InvalidAuthorizationSpecification = ErrorCode("28000") // Class 28 - Invalid Authorization Specification - InvalidPassword = ErrorCode("28P01") - DependentPrivilegeDescriptorsStillExist = ErrorCode("2B000") // Class 2B - Dependent Privilege Descriptors Still Exist - DependentObjectsStillExist = ErrorCode("2BP01") - InvalidTransactionTermination = ErrorCode("2D000") // Class 2D - Invalid Transaction Termination - SQLRoutineException = ErrorCode("2F000") // Class 2F - SQL Routine Exception - SREFunctionExecutedNoReturnStatement = ErrorCode("2F005") - SREModifyingSQLDataNotPermitted = ErrorCode("2F002") - SREProhibitedSQLStatementAttempted = ErrorCode("2F003") - SREReadingSQLDataNotPermitted = ErrorCode("2F004") - InvalidCursorName = ErrorCode("34000") // Class 34 - Invalid Cursor Name - ExternalRoutineException = ErrorCode("38000") // Class 38 - External Routine Exception - EREContainingSQLNotPermitted = ErrorCode("38001") - EREModifyingSQLDataNotPermitted = ErrorCode("38002") - EREProhibitedSQLStatementAttempted = ErrorCode("38003") - EREReadingSQLDataNotPermitted = ErrorCode("38004") - ExternalRoutineInvocationException = ErrorCode("39000") // Class 39 - External Routine Invocation Exception - ERIEInvalidSQLSTATEReturned = ErrorCode("39001") - ERIENullValueNotAllowed = ErrorCode("39004") - ERIETriggerProtocolViolated = ErrorCode("39P01") - ERIESrfProtocolViolated = ErrorCode("39P02") - ERIEEventTriggerProtocolViolated = ErrorCode("39P03") - SavepointException = ErrorCode("3B000") // Class 3B - Savepoint Exception - SEInvalidSpecification = ErrorCode("3B001") - InvalidCatalogName = ErrorCode("3D000") // Class 3D - Invalid Catalog Name - InvalidSchemaName = ErrorCode("3F000") // Class 3F - Invalid Schema Name - TransactionRollback = ErrorCode("40000") // Class 40 - Transaction Rollback - TRIntegrityConstraintViolation = ErrorCode("40002") - TRSerializationFailure = ErrorCode("40001") - TRStatementCompletionUnknown = ErrorCode("40003") - TRDeadlockDetected = ErrorCode("40P01") - SyntaxErrorOrAccessRuleViolation = ErrorCode("42000") // Class 42 - Syntax Error or Access Rule Violation - SyntaxError = ErrorCode("42601") - InsufficientPrivilege = ErrorCode("42501") - CannotCoerce = ErrorCode("42846") - GroupingError = ErrorCode("42803") - WindowingError = ErrorCode("42P20") - InvalidRecursion = ErrorCode("42P19") - InvalidForeignKey = ErrorCode("42830") - InvalidName = ErrorCode("42602") - NameTooLong = ErrorCode("42622") - ReservedName = ErrorCode("42939") - DatatypeMismatch = ErrorCode("42804") - IndeterminateDatatype = ErrorCode("42P18") - CollationMismatch = ErrorCode("42P21") - IndeterminateCollation = ErrorCode("42P22") - WrongObjectType = ErrorCode("42809") - GeneratedAlways = ErrorCode("428C9") - UndefinedColumn = ErrorCode("42703") - UndefinedFunction = ErrorCode("42883") - UndefinedTable = ErrorCode("42P01") - UndefinedParameter = ErrorCode("42P02") - UndefinedObject = ErrorCode("42704") - DuplicateColumn = ErrorCode("42701") - DuplicateCursor = ErrorCode("42P03") - DuplicateDatabase = ErrorCode("42P04") - DuplicateFunction = ErrorCode("42723") - DuplicatePstatement = ErrorCode("42P05") - DuplicateSchema = ErrorCode("42P06") - DuplicateTable = ErrorCode("42P07") - DuplicateAlias = ErrorCode("42712") - DuplicateObject = ErrorCode("42710") - AmbiguousColumn = ErrorCode("42702") - AmbiguousFunction = ErrorCode("42725") - AmbiguousParameter = ErrorCode("42P08") - AmbiguousAlias = ErrorCode("42P09") - InvalidColumnReference = ErrorCode("42P10") - InvalidColumnDefinition = ErrorCode("42611") - InvalidCursorDefinition = ErrorCode("42P11") - InvalidDatabaseDefinition = ErrorCode("42P12") - InvalidFunctionDefinition = ErrorCode("42P13") - InvalidPstatementDefinition = ErrorCode("42P14") - InvalidSchemaDefinition = ErrorCode("42P15") - InvalidTableDefinition = ErrorCode("42P16") - InvalidObjectDefinition = ErrorCode("42P17") - WithCheckOptionViolation = ErrorCode("44000") // Class 44 - WITH CHECK OPTION Violation - InsufficientResources = ErrorCode("53000") // Class 53 - Insufficient Resources - DiskFull = ErrorCode("53100") - OutOfMemory = ErrorCode("53200") - TooManyConnections = ErrorCode("53300") - ConfigurationLimitExceeded = ErrorCode("53400") - ProgramLimitExceeded = ErrorCode("54000") // Class 54 - Program Limit Exceeded - StatementTooComplex = ErrorCode("54001") - TooManyColumns = ErrorCode("54011") - TooManyArguments = ErrorCode("54023") - ObjectNotInPrerequisiteState = ErrorCode("55000") // Class 55 - Object Not In Prerequisite State - ObjectInUse = ErrorCode("55006") - CantChangeRuntimeParam = ErrorCode("55P02") - LockNotAvailable = ErrorCode("55P03") - UnsafeNewEnumValueUsage = ErrorCode("55P04") - OperatorIntervention = ErrorCode("57000") // Class 57 - Operator Intervention - QueryCanceled = ErrorCode("57014") - AdminShutdown = ErrorCode("57P01") - CrashShutdown = ErrorCode("57P02") - CannotConnectNow = ErrorCode("57P03") - DatabaseDropped = ErrorCode("57P04") - IdleSessionTimeout = ErrorCode("57P05") - SystemError = ErrorCode("58000") // Class 58 - System Error (errors external to PostgreSQL itself) - IOError = ErrorCode("58030") - UndefinedFile = ErrorCode("58P01") - DuplicateFile = ErrorCode("58P02") - FileNameTooLong = ErrorCode("58P03") - ConfigFileError = ErrorCode("F0000") // Class F0 - Configuration File Error - LockFileExists = ErrorCode("F0001") - FDWError = ErrorCode("HV000") // Class HV - Foreign Data Wrapper Error (SQL/MED) - FDWColumnNameNotFound = ErrorCode("HV005") - FDWDynamicParameterValueNeeded = ErrorCode("HV002") - FDWFunctionSequenceError = ErrorCode("HV010") - FDWInconsistentDescriptorInformation = ErrorCode("HV021") - FDWInvalidAttributeValue = ErrorCode("HV024") - FDWInvalidColumnName = ErrorCode("HV007") - FDWInvalidColumnNumber = ErrorCode("HV008") - FDWInvalidDataType = ErrorCode("HV004") - FDWInvalidDataTypeDescriptors = ErrorCode("HV006") - FDWInvalidDescriptorFieldIdentifier = ErrorCode("HV091") - FDWInvalidHandle = ErrorCode("HV00B") - FDWInvalidOptionIndex = ErrorCode("HV00C") - FDWInvalidOptionName = ErrorCode("HV00D") - FDWInvalidStringLengthOrBufferLength = ErrorCode("HV090") - FDWInvalidStringFormat = ErrorCode("HV00A") - FDWInvalidUseOfNullPointer = ErrorCode("HV009") - FDWTooManyHandles = ErrorCode("HV014") - FDWOutOfMemory = ErrorCode("HV001") - FDWNoSchemas = ErrorCode("HV00P") - FDWOptionNameNotFound = ErrorCode("HV00J") - FDWReplyHandle = ErrorCode("HV00K") - FDWSchemaNotFound = ErrorCode("HV00Q") - FDWTableNotFound = ErrorCode("HV00R") - FDWUnableToCreateExecution = ErrorCode("HV00L") - FDWUnableToCreateReply = ErrorCode("HV00M") - FDWUnableToEstablishConnection = ErrorCode("HV00N") - PLpgSQLError = ErrorCode("P0000") // Class P0 - PL/pgSQL Error - RaiseException = ErrorCode("P0001") - NoDataFound = ErrorCode("P0002") - TooManyRows = ErrorCode("P0003") - AssertFailure = ErrorCode("P0004") - InternalError = ErrorCode("XX000") // Class XX - Internal Error - DataCorrupted = ErrorCode("XX001") - IndexCorrupted = ErrorCode("XX002") + SuccessfulCompletion = Code("00000") // Class 00 - Successful Completion + Warning = Code("01000") // Class 01 - Warning + WarningDynamicResultSetsReturned = Code("0100C") + WarningImplicitZeroBitPadding = Code("01008") + WarningNullValueEliminatedInSetFunction = Code("01003") + WarningPrivilegeNotGranted = Code("01007") + WarningPrivilegeNotRevoked = Code("01006") + WarningStringDataRightTruncation = Code("01004") + WarningDeprecatedFeature = Code("01P01") + NoData = Code("02000") // Class 02 - No Data (this is also a warning class per the SQL standard) + NoAdditionalDynamicResultSetsReturned = Code("02001") + SQLStatementNotYetComplete = Code("03000") // Class 03 - SQL Statement Not Yet Complete + ConnectionException = Code("08000") // Class 08 - Connection Exception + ConnectionDoesNotExist = Code("08003") + ConnectionFailure = Code("08006") + SQLClientUnableToEstablishSQLConnection = Code("08001") + SQLServerRejectedEstablishmentOfSQLConnection = Code("08004") + TransactionResolutionUnknown = Code("08007") + ProtocolViolation = Code("08P01") + TriggeredActionException = Code("09000") // Class 09 - Triggered Action Exception + FeatureNotSupported = Code("0A000") // Class 0A - Feature Not Supported + InvalidTransactionInitiation = Code("0B000") // Class 0B - Invalid Transaction Initiation + LocatorException = Code("0F000") // Class 0F - Locator Exception + LEInvalidSpecification = Code("0F001") + InvalidGrantor = Code("0L000") // Class 0L - Invalid Grantor + InvalidGrantOperation = Code("0LP01") + InvalidRoleSpecification = Code("0P000") // Class 0P - Invalid Role Specification + DiagnosticsException = Code("0Z000") // Class 0Z - Diagnostics Exception + StackedDiagnosticsAccessedWithoutActiveHandler = Code("0Z002") + InvalidArgumentForXquery = Code("10608") + CaseNotFound = Code("20000") // Class 20 - Case Not Found + CardinalityViolation = Code("21000") // Class 21 - Cardinality Violation + DataException = Code("22000") // Class 22 - Data Exception + ArraySubscriptError = Code("2202E") + CharacterNotInRepertoire = Code("22021") + DatetimeFieldOverflow = Code("22008") + DivisionByZero = Code("22012") + ErrorInAssignment = Code("22005") + EscapeCharacterConflict = Code("2200B") + IndicatorOverflow = Code("22022") + IntervalFieldOverflow = Code("22015") + InvalidArgumentForLog = Code("2201E") + InvalidArgumentForNtile = Code("22014") + InvalidArgumentForNthValue = Code("22016") + InvalidArgumentForPowerFunction = Code("2201F") + InvalidArgumentForWidthBucketFunction = Code("2201G") + InvalidCharacterValueForCast = Code("22018") + InvalidDatetimeFormat = Code("22007") + InvalidEscapeCharacter = Code("22019") + InvalidEscapeOctet = Code("2200D") + InvalidEscapeSequence = Code("22025") + NonstandardUseOfEscapeCharacter = Code("22P06") + InvalidIndicatorParameterValue = Code("22010") + InvalidParameterValue = Code("22023") + InvalidPrecedingOrFollowingSize = Code("22013") + InvalidRegularExpression = Code("2201B") + InvalidRowCountInLimitClause = Code("2201W") + InvalidRowCountInResultOffsetClause = Code("2201X") + InvalidTablesampleArgument = Code("2202H") + InvalidTablesampleRepeat = Code("2202G") + InvalidTimeZoneDisplacementValue = Code("22009") + InvalidUseOfEscapeCharacter = Code("2200C") + MostSpecificTypeMismatch = Code("2200G") + NullValueNotAllowed = Code("22004") + NullValueNoIndicatorParameter = Code("22002") + NumericValueOutOfRange = Code("22003") + SequenceGeneratorLimitExceeded = Code("2200H") + StringDataLengthMismatch = Code("22026") + StringDataRightTruncation = Code("22001") + SubstringError = Code("22011") + TrimError = Code("22027") + UnterminatedCString = Code("22024") + ZeroLengthCharacterString = Code("2200F") + FloatingPointException = Code("22P01") + InvalidTextRepresentation = Code("22P02") + InvalidBinaryRepresentation = Code("22P03") + BadCopyFileFormat = Code("22P04") + UntranslatableCharacter = Code("22P05") + NotAnXMLDocument = Code("2200L") + InvalidXMLDocument = Code("2200M") + InvalidXMLContent = Code("2200N") + InvalidXMLComment = Code("2200S") + InvalidXMLProcessingInstruction = Code("2200T") + DuplicateJSONObjectKeyValue = Code("22030") + InvalidArgumentForSQLJSONDatetimeFunction = Code("22031") + InvalidJSONText = Code("22032") + InvalidSQLJSONSubscript = Code("22033") + MoreThanOneSQLJSONItem = Code("22034") + NoSQLJSONItem = Code("22035") + NonNumericSQLJSONItem = Code("22036") + NonUniqueKeysInAJSONObject = Code("22037") + SingletonSQLJSONItemRequired = Code("22038") + SQLJSONArrayNotFound = Code("22039") + SQLJSONMemberNotFound = Code("2203A") + SQLJSONNumberNotFound = Code("2203B") + SQLJSONObjectNotFound = Code("2203C") + TooManyJSONArrayElements = Code("2203D") + TooManyJSONObjectMembers = Code("2203E") + SQLJSONScalarRequired = Code("2203F") + SQLJSONItemCannotBeCastToTargetType = Code("2203G") + IntegrityConstraintViolation = Code("23000") // Class 23 - Integrity Constraint Violation + RestrictViolation = Code("23001") + NotNullViolation = Code("23502") + ForeignKeyViolation = Code("23503") + UniqueViolation = Code("23505") + CheckViolation = Code("23514") + ExclusionViolation = Code("23P01") + InvalidCursorState = Code("24000") // Class 24 - Invalid Cursor State + InvalidTransactionState = Code("25000") // Class 25 - Invalid Transaction State + ActiveSQLTransaction = Code("25001") + BranchTransactionAlreadyActive = Code("25002") + HeldCursorRequiresSameIsolationLevel = Code("25008") + InappropriateAccessModeForBranchTransaction = Code("25003") + InappropriateIsolationLevelForBranchTransaction = Code("25004") + NoActiveSQLTransactionForBranchTransaction = Code("25005") + ReadOnlySQLTransaction = Code("25006") + SchemaAndDataStatementMixingNotSupported = Code("25007") + NoActiveSQLTransaction = Code("25P01") + InFailedSQLTransaction = Code("25P02") + IdleInTransactionSessionTimeout = Code("25P03") + TransactionTimeout = Code("25P04") + InvalidSQLStatementName = Code("26000") // Class 26 - Invalid SQL Statement Name + TriggeredDataChangeViolation = Code("27000") // Class 27 - Triggered Data Change Violation + InvalidAuthorizationSpecification = Code("28000") // Class 28 - Invalid Authorization Specification + InvalidPassword = Code("28P01") + DependentPrivilegeDescriptorsStillExist = Code("2B000") // Class 2B - Dependent Privilege Descriptors Still Exist + DependentObjectsStillExist = Code("2BP01") + InvalidTransactionTermination = Code("2D000") // Class 2D - Invalid Transaction Termination + SQLRoutineException = Code("2F000") // Class 2F - SQL Routine Exception + SREFunctionExecutedNoReturnStatement = Code("2F005") + SREModifyingSQLDataNotPermitted = Code("2F002") + SREProhibitedSQLStatementAttempted = Code("2F003") + SREReadingSQLDataNotPermitted = Code("2F004") + InvalidCursorName = Code("34000") // Class 34 - Invalid Cursor Name + ExternalRoutineException = Code("38000") // Class 38 - External Routine Exception + EREContainingSQLNotPermitted = Code("38001") + EREModifyingSQLDataNotPermitted = Code("38002") + EREProhibitedSQLStatementAttempted = Code("38003") + EREReadingSQLDataNotPermitted = Code("38004") + ExternalRoutineInvocationException = Code("39000") // Class 39 - External Routine Invocation Exception + ERIEInvalidSQLSTATEReturned = Code("39001") + ERIENullValueNotAllowed = Code("39004") + ERIETriggerProtocolViolated = Code("39P01") + ERIESrfProtocolViolated = Code("39P02") + ERIEEventTriggerProtocolViolated = Code("39P03") + SavepointException = Code("3B000") // Class 3B - Savepoint Exception + SEInvalidSpecification = Code("3B001") + InvalidCatalogName = Code("3D000") // Class 3D - Invalid Catalog Name + InvalidSchemaName = Code("3F000") // Class 3F - Invalid Schema Name + TransactionRollback = Code("40000") // Class 40 - Transaction Rollback + TRIntegrityConstraintViolation = Code("40002") + TRSerializationFailure = Code("40001") + TRStatementCompletionUnknown = Code("40003") + TRDeadlockDetected = Code("40P01") + SyntaxErrorOrAccessRuleViolation = Code("42000") // Class 42 - Syntax Error or Access Rule Violation + SyntaxError = Code("42601") + InsufficientPrivilege = Code("42501") + CannotCoerce = Code("42846") + GroupingError = Code("42803") + WindowingError = Code("42P20") + InvalidRecursion = Code("42P19") + InvalidForeignKey = Code("42830") + InvalidName = Code("42602") + NameTooLong = Code("42622") + ReservedName = Code("42939") + DatatypeMismatch = Code("42804") + IndeterminateDatatype = Code("42P18") + CollationMismatch = Code("42P21") + IndeterminateCollation = Code("42P22") + WrongObjectType = Code("42809") + GeneratedAlways = Code("428C9") + UndefinedColumn = Code("42703") + UndefinedFunction = Code("42883") + UndefinedTable = Code("42P01") + UndefinedParameter = Code("42P02") + UndefinedObject = Code("42704") + DuplicateColumn = Code("42701") + DuplicateCursor = Code("42P03") + DuplicateDatabase = Code("42P04") + DuplicateFunction = Code("42723") + DuplicatePstatement = Code("42P05") + DuplicateSchema = Code("42P06") + DuplicateTable = Code("42P07") + DuplicateAlias = Code("42712") + DuplicateObject = Code("42710") + AmbiguousColumn = Code("42702") + AmbiguousFunction = Code("42725") + AmbiguousParameter = Code("42P08") + AmbiguousAlias = Code("42P09") + InvalidColumnReference = Code("42P10") + InvalidColumnDefinition = Code("42611") + InvalidCursorDefinition = Code("42P11") + InvalidDatabaseDefinition = Code("42P12") + InvalidFunctionDefinition = Code("42P13") + InvalidPstatementDefinition = Code("42P14") + InvalidSchemaDefinition = Code("42P15") + InvalidTableDefinition = Code("42P16") + InvalidObjectDefinition = Code("42P17") + WithCheckOptionViolation = Code("44000") // Class 44 - WITH CHECK OPTION Violation + InsufficientResources = Code("53000") // Class 53 - Insufficient Resources + DiskFull = Code("53100") + OutOfMemory = Code("53200") + TooManyConnections = Code("53300") + ConfigurationLimitExceeded = Code("53400") + ProgramLimitExceeded = Code("54000") // Class 54 - Program Limit Exceeded + StatementTooComplex = Code("54001") + TooManyColumns = Code("54011") + TooManyArguments = Code("54023") + ObjectNotInPrerequisiteState = Code("55000") // Class 55 - Object Not In Prerequisite State + ObjectInUse = Code("55006") + CantChangeRuntimeParam = Code("55P02") + LockNotAvailable = Code("55P03") + UnsafeNewEnumValueUsage = Code("55P04") + OperatorIntervention = Code("57000") // Class 57 - Operator Intervention + QueryCanceled = Code("57014") + AdminShutdown = Code("57P01") + CrashShutdown = Code("57P02") + CannotConnectNow = Code("57P03") + DatabaseDropped = Code("57P04") + IdleSessionTimeout = Code("57P05") + SystemError = Code("58000") // Class 58 - System Error (errors external to PostgreSQL itself) + IOError = Code("58030") + UndefinedFile = Code("58P01") + DuplicateFile = Code("58P02") + FileNameTooLong = Code("58P03") + ConfigFileError = Code("F0000") // Class F0 - Configuration File Error + LockFileExists = Code("F0001") + FDWError = Code("HV000") // Class HV - Foreign Data Wrapper Error (SQL/MED) + FDWColumnNameNotFound = Code("HV005") + FDWDynamicParameterValueNeeded = Code("HV002") + FDWFunctionSequenceError = Code("HV010") + FDWInconsistentDescriptorInformation = Code("HV021") + FDWInvalidAttributeValue = Code("HV024") + FDWInvalidColumnName = Code("HV007") + FDWInvalidColumnNumber = Code("HV008") + FDWInvalidDataType = Code("HV004") + FDWInvalidDataTypeDescriptors = Code("HV006") + FDWInvalidDescriptorFieldIdentifier = Code("HV091") + FDWInvalidHandle = Code("HV00B") + FDWInvalidOptionIndex = Code("HV00C") + FDWInvalidOptionName = Code("HV00D") + FDWInvalidStringLengthOrBufferLength = Code("HV090") + FDWInvalidStringFormat = Code("HV00A") + FDWInvalidUseOfNullPointer = Code("HV009") + FDWTooManyHandles = Code("HV014") + FDWOutOfMemory = Code("HV001") + FDWNoSchemas = Code("HV00P") + FDWOptionNameNotFound = Code("HV00J") + FDWReplyHandle = Code("HV00K") + FDWSchemaNotFound = Code("HV00Q") + FDWTableNotFound = Code("HV00R") + FDWUnableToCreateExecution = Code("HV00L") + FDWUnableToCreateReply = Code("HV00M") + FDWUnableToEstablishConnection = Code("HV00N") + PLpgSQLError = Code("P0000") // Class P0 - PL/pgSQL Error + RaiseException = Code("P0001") + NoDataFound = Code("P0002") + TooManyRows = Code("P0003") + AssertFailure = Code("P0004") + InternalError = Code("XX000") // Class XX - Internal Error + DataCorrupted = Code("XX001") + IndexCorrupted = Code("XX002") ) -var errorCodeNames = map[ErrorCode]string{ +var errorCodeNames = map[Code]string{ "00000": "successful_completion", "01000": "warning", "0100C": "dynamic_result_sets_returned", diff --git a/pqerror/deprecated.go b/pqerror/deprecated.go deleted file mode 100644 index d678caa03..000000000 --- a/pqerror/deprecated.go +++ /dev/null @@ -1,44 +0,0 @@ -package pqerror - -// Get implements the legacy PGError interface. -// -// Deprecated: new code should use the fields of the Error struct directly. -func (e *Error) Get(k byte) (v string) { - switch k { - case 'S': - return e.Severity - case 'C': - return string(e.Code) - case 'M': - return e.Message - case 'D': - return e.Detail - case 'H': - return e.Hint - case 'P': - return e.Position - case 'p': - return e.InternalPosition - case 'q': - return e.InternalQuery - case 'W': - return e.Where - case 's': - return e.Schema - case 't': - return e.Table - case 'c': - return e.Column - case 'd': - return e.DataTypeName - case 'n': - return e.Constraint - case 'F': - return e.File - case 'L': - return e.Line - case 'R': - return e.Routine - } - return "" -} diff --git a/pqerror/example_test.go b/pqerror/example_test.go deleted file mode 100644 index e74bc3291..000000000 --- a/pqerror/example_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package pqerror_test - -import ( - "database/sql" - "log" - - "github.com/lib/pq/pqerror" -) - -func ExampleAs() { - db, err := sql.Open("postgres", "") - if err != nil { - log.Fatal(err) - } - - email := "hello@example.com" - - _, err = db.Exec("insert into t (email) values ($1)", email) - if pqErr := pqerror.As(err, pqerror.UniqueViolation); pqErr != nil { - log.Fatalf("email %q already exsts", email) - } - if err != nil { - log.Fatalf("unknown error: %s", err) - } -} diff --git a/pqerror/gen.go b/pqerror/gen.go index 47c8c3c28..e3b225167 100644 --- a/pqerror/gen.go +++ b/pqerror/gen.go @@ -109,12 +109,12 @@ func main() { class := strings.HasSuffix(sqlstate, "000") if class { - fmt.Fprintf(out, "\tClass%s = ErrorClass(%q) // %s\n", name, sqlstate[:2], section[11:]) + fmt.Fprintf(out, "\tClass%s = Class(%q) // %s\n", name, sqlstate[:2], section[11:]) } fmt.Fprintf(names, "\t%q: %q,\n", sqlstate, spec) - fmt.Fprintf(codes, "\t%s = ErrorCode(%q)", name, sqlstate) + fmt.Fprintf(codes, "\t%s = Code(%q)", name, sqlstate) if class { fmt.Fprintf(codes, " // %s", section) } @@ -130,7 +130,7 @@ func main() { out.Write(codes.Bytes()) out.WriteString(")\n\n") - out.WriteString("var errorCodeNames = map[ErrorCode]string{\n") + out.WriteString("var errorCodeNames = map[Code]string{\n") out.Write(names.Bytes()) out.WriteString("}\n") diff --git a/pqerror/pqerror.go b/pqerror/pqerror.go index f3d92df61..29e49e999 100644 --- a/pqerror/pqerror.go +++ b/pqerror/pqerror.go @@ -1,247 +1,35 @@ //go:generate go run gen.go +// Package pqerror contains PostgreSQL error codes for use with pq.Error. package pqerror -import ( - "fmt" - "strconv" - "strings" - "unicode/utf8" -) - -// [pqerror.Error.Severity] values. -const ( - SeverityFatal = "FATAL" - SeverityPanic = "PANIC" - SeverityWarning = "WARNING" - SeverityNotice = "NOTICE" - SeverityDebug = "DEBUG" - SeverityInfo = "INFO" - SeverityLog = "LOG" -) - -type Error struct { - // [Efatal], [Epanic], [Ewarning], [Enotice], [Edebug], [Einfo], or [Elog]. - // Always present. - Severity string - - // SQLSTATE code. Always present. - Code ErrorCode - - // Primary human-readable error message. This should be accurate but terse - // (typically one line). Always present. - Message string - - // Optional secondary error message carrying more detail about the problem. - // Might run to multiple lines. - Detail string - - // Optional suggestion what to do about the problem. This is intended to - // differ from Detail in that it offers advice (potentially inappropriate) - // rather than hard facts. Might run to multiple lines. - Hint string - - // error position as an index into the original query string, as decimal - // ASCII integer. The first character has index 1, and positions are - // measured in characters not bytes. - Position string - - // This is defined the same as the Position field, but it is used when the - // cursor position refers to an internally generated command rather than the - // one submitted by the client. The InternalQuery field will always appear - // when this field appears. - InternalPosition string - - // Text of a failed internally-generated command. This could be, for - // example, an SQL query issued by a PL/pgSQL function. - InternalQuery string - - // An indication of the context in which the error occurred. Presently this - // includes a call stack traceback of active procedural language functions - // and internally-generated queries. The trace is one entry per line, most - // recent first. - Where string - - // If the error was associated with a specific database object, the name of - // the schema containing that object, if any. - Schema string - - // If the error was associated with a specific table, the name of the table. - // (Refer to the schema name field for the name of the table's schema.) - Table string - - // If the error was associated with a specific table column, the name of the - // column. (Refer to the schema and table name fields to identify the - // table.) - Column string - - // If the error was associated with a specific data type, the name of the - // data type. (Refer to the schema name field for the name of the data - // type's schema.) - DataTypeName string - - // If the error was associated with a specific constraint, the name of the - // constraint. Refer to fields listed above for the associated table or - // domain. (For this purpose, indexes are treated as constraints, even if - // they weren't created with constraint syntax.) - Constraint string - - // File name of the source-code location where the error was reported. - File string - - // Line number of the source-code location where the error was reported. - Line string - - // Name of the source-code routine reporting the error. - Routine string - - // Query is the original query. This is set by pq, and not PostgreSQL. - Query string -} - -type ( - // ErrorCode is a five-character error code. - ErrorCode string - - // ErrorClass is only the class part of an error code. - ErrorClass string -) +// Code is a five-character error code. +type Code string // Name returns a more human friendly rendering of the error code, namely the // "condition name". -// -// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for -// details. -func (ec ErrorCode) Name() string { return errorCodeNames[ec] } +func (ec Code) Name() string { return errorCodeNames[ec] } // Class returns the error class, e.g. "28". -// -// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for -// details. -func (ec ErrorCode) Class() ErrorClass { return ErrorClass(ec[:2]) } +func (ec Code) Class() Class { return Class(ec[:2]) } + +// Class is only the class part of an error code. +type Class string // Name returns the condition name of an error class. It is equivalent to the // condition name of the "standard" error code (i.e. the one having the last // three characters "000"). -func (ec ErrorClass) Name() string { return errorCodeNames[ErrorCode(ec+"000")] } - -// Fatal returns true if the Error Severity is fatal. -func (e *Error) Fatal() bool { return e.Severity == SeverityFatal } - -// SQLState returns the SQLState of the error. -func (e *Error) SQLState() string { return string(e.Code) } - -func (e *Error) Error() string { - msg := e.Message - if e.Query != "" && e.Position != "" { - pos, err := strconv.Atoi(e.Position) - if err == nil { - lines := strings.Split(e.Query, "\n") - line, col := posToLine(pos, lines) - if len(lines) == 1 { - msg += " at column " + strconv.Itoa(col) - } else { - msg += " at position " + strconv.Itoa(line) + ":" + strconv.Itoa(col) - } - } - } - - if e.Code != "" { - return "pq: " + msg + " (" + string(e.Code) + ")" - } - return "pq: " + msg -} - -// ErrorWithDetail returns the error message with detailed information and -// location context (if any). -// -// See the documentation on [Error]. -func (e *Error) ErrorWithDetail() string { - b := new(strings.Builder) - b.Grow(len(e.Message) + len(e.Detail) + len(e.Hint) + 30) - b.WriteString("ERROR: ") - b.WriteString(e.Message) - if e.Code != "" { - b.WriteString(" (") - b.WriteString(string(e.Code)) - b.WriteByte(')') - } - if e.Detail != "" { - b.WriteString("\nDETAIL: ") - b.WriteString(e.Detail) - } - if e.Hint != "" { - b.WriteString("\nHINT: ") - b.WriteString(e.Hint) - } +func (ec Class) Name() string { return errorCodeNames[Code(ec+"000")] } - if e.Query != "" && e.Position != "" { - b.Grow(512) - pos, err := strconv.Atoi(e.Position) - if err != nil { - return b.String() - } - lines := strings.Split(e.Query, "\n") - line, col := posToLine(pos, lines) +// TODO(v2): use "type Severity string" for the below. - fmt.Fprintf(b, "\nCONTEXT: line %d, column %d:\n\n", line, col) - if line > 2 { - fmt.Fprintf(b, "% 7d | %s\n", line-2, expandTab(lines[line-3])) - } - if line > 1 { - fmt.Fprintf(b, "% 7d | %s\n", line-1, expandTab(lines[line-2])) - } - /// Expand tabs, so that the ^ is at at the correct position, but leave - /// "column 10-13" intact. Adjusting this to the visual column would be - /// better, but we don't know the tabsize of the user in their editor, - /// which can be 8, 4, 2, or something else. We can't know. So leaving - /// it as the character index is probably the "most correct". - expanded := expandTab(lines[line-1]) - diff := len(expanded) - len(lines[line-1]) - fmt.Fprintf(b, "% 7d | %s\n", line, expanded) - fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col-1+diff), "^") - } - - return b.String() -} - -func posToLine(pos int, lines []string) (line, col int) { - read := 0 - for i := range lines { - line++ - ll := utf8.RuneCountInString(lines[i]) + 1 // +1 for the removed newline - if read+ll >= pos { - col = max(pos-read, 1) // Should be lower than 1, but just in case. - break - } - read += ll - } - return line, col -} - -func expandTab(s string) string { - var ( - b strings.Builder - l int - fill = func(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = ' ' - } - return string(b) - } - ) - b.Grow(len(s)) - for _, r := range s { - switch r { - case '\t': - tw := 8 - l%8 - b.WriteString(fill(tw)) - l += tw - default: - b.WriteRune(r) - l += 1 - } - } - return b.String() -} +// Error severity values. +const ( + SeverityFatal = "FATAL" + SeverityPanic = "PANIC" + SeverityWarning = "WARNING" + SeverityNotice = "NOTICE" + SeverityDebug = "DEBUG" + SeverityInfo = "INFO" + SeverityLog = "LOG" +) diff --git a/pqerror/pqerror_test.go b/pqerror/pqerror_test.go deleted file mode 100644 index 678e263a4..000000000 --- a/pqerror/pqerror_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package pqerror - -import ( - "errors" - "os" - "reflect" - "testing" -) - -func TestAs(t *testing.T) { - tests := []struct { - err error - codes []ErrorCode - wantReturn bool - }{ - {nil, nil, false}, - {nil, []ErrorCode{SyntaxError}, false}, - {errors.New("oh noes"), []ErrorCode{SyntaxError}, false}, - {&Error{Code: "00000", Message: "okay"}, nil, true}, - - {&Error{Code: "00000", Message: "okay"}, []ErrorCode{SyntaxError}, false}, - {&Error{Code: "00000", Message: "okay"}, []ErrorCode{SyntaxError, SuccessfulCompletion}, true}, - } - - //t.Parallel() - for _, tt := range tests { - t.Run("", func(t *testing.T) { - have := As(tt.err, tt.codes...) - if tt.wantReturn { - if !reflect.DeepEqual(have, tt.err) { - t.Errorf("\nhave: %#v\nwant: %#v", have, tt.err) - } - } else { - if have != nil { - t.Errorf("expected return to be nil, but have:\n%#v", have) - } - } - }) - } -} - -func BenchmarkAs(b *testing.B) { - b.Run("nil", func(b *testing.B) { - var nilerr error - for i := 0; i < b.N; i++ { - _ = As(nilerr, SuccessfulCompletion) - } - }) - b.Run("other error", func(b *testing.B) { - patherr := &os.PathError{} - for i := 0; i < b.N; i++ { - _ = As(patherr, SuccessfulCompletion) - } - }) - b.Run("pq.Error", func(b *testing.B) { - pqerr := &Error{Code: "00000", Message: "okay"} - for i := 0; i < b.N; i++ { - _ = As(pqerr, SuccessfulCompletion) - } - }) -} From dec9ada4f35ca4ed522149c622fa4c33fbf2182a Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 17 Mar 2026 23:08:30 +0000 Subject: [PATCH 3/3] Rewrite tests to use pqerror, pq.As() --- conn_test.go | 310 +++++++++++++------------------------------------ helper_test.go | 18 ++- issues_test.go | 21 ++-- ssl_test.go | 2 +- 4 files changed, 99 insertions(+), 252 deletions(-) diff --git a/conn_test.go b/conn_test.go index 2883b28d5..381c2d535 100644 --- a/conn_test.go +++ b/conn_test.go @@ -6,7 +6,6 @@ import ( "database/sql" "database/sql/driver" "encoding/json" - "errors" "fmt" "io" "math" @@ -22,6 +21,7 @@ import ( "github.com/lib/pq/internal/pqtest" "github.com/lib/pq/internal/pqutil" "github.com/lib/pq/internal/proto" + "github.com/lib/pq/pqerror" ) func TestReconnect(t *testing.T) { @@ -59,14 +59,14 @@ func TestReconnect(t *testing.T) { func TestCommitInFailedTransaction(t *testing.T) { db := pqtest.MustDB(t) - txn := pqtest.Begin(t, db) + tx := pqtest.Begin(t, db) - rows, err := txn.Query("SELECT error") + rows, err := tx.Query("select error") if err == nil { rows.Close() t.Fatal("expected failure") } - err = txn.Commit() + err = tx.Commit() if err != ErrInFailedTransaction { t.Fatalf("expected ErrInFailedTransaction; got %#v", err) } @@ -575,18 +575,7 @@ func TestErrorDuringStartup(t *testing.T) { // Don't use the normal connection setup, this is intended to blow up in the // startup packet from a non-existent user. _, err := pqtest.DB(t, "user=thisuserreallydoesntexist") - - if err == nil { - t.Fatal("expected error") - } - - e, ok := err.(*Error) - if !ok { - t.Fatalf("wrong error type %T: %[1]s", err) - } - if e.Code.Name() != "invalid_authorization_specification" && e.Code.Name() != "invalid_password" { - t.Fatalf("wrong error code %q: %s", e.Code.Name(), err) - } + mustAs(t, err, pqerror.InvalidAuthorizationSpecification, pqerror.InvalidPassword) } type testConn struct { @@ -685,17 +674,8 @@ func TestConnClose(t *testing.T) { t.Fatal(err) } - // During the Go 1.9 cycle, https://github.com/golang/go/commit/3792db5 - // changed this error from - // - // net.errClosing = errors.New("use of closed network connection") - // - // to - // - // internal/poll.ErrClosing = errors.New("use of closed file or network connection") - const errClosing = "use of closed" - // Verify write after closing fails. + const errClosing = "use of closed" _, err = nc.Write(nil) if err == nil { t.Fatal("expected error") @@ -716,84 +696,33 @@ func TestConnClose(t *testing.T) { func TestErrorOnExec(t *testing.T) { db := pqtest.MustDB(t) + tx := pqtest.Begin(t, db) - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer txn.Rollback() - - _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") - if err != nil { - t.Fatal(err) - } + pqtest.Exec(t, tx, `create temp table foo(f1 int primary key)`) - _, err = txn.Exec("INSERT INTO foo VALUES (0), (0)") - if err == nil { - t.Fatal("Should have raised error") - } - - e, ok := err.(*Error) - if !ok { - t.Fatalf("expected Error, got %#v", err) - } else if e.Code.Name() != "unique_violation" { - t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) - } + _, err := tx.Exec("insert into foo values (0), (0)") + mustAs(t, err, pqerror.UniqueViolation) } func TestErrorOnQuery(t *testing.T) { db := pqtest.MustDB(t) + tx := pqtest.Begin(t, db) - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer txn.Rollback() - - _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") - if err != nil { - t.Fatal(err) - } - - _, err = txn.Query("INSERT INTO foo VALUES (0), (0)") - if err == nil { - t.Fatal("Should have raised error") - } + pqtest.Exec(t, tx, `create temp table foo(f1 int primary key)`) - e, ok := err.(*Error) - if !ok { - t.Fatalf("expected Error, got %#v", err) - } else if e.Code.Name() != "unique_violation" { - t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) - } + _, err := tx.Query("insert into foo values (0), (0)") + mustAs(t, err, pqerror.UniqueViolation) } func TestErrorOnQueryRowSimpleQuery(t *testing.T) { db := pqtest.MustDB(t) + tx := pqtest.Begin(t, db) - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer txn.Rollback() - - _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") - if err != nil { - t.Fatal(err) - } + pqtest.Exec(t, tx, `create temp table foo(f1 int primary key)`) var v int - err = txn.QueryRow("INSERT INTO foo VALUES (0), (0)").Scan(&v) - if err == nil { - t.Fatal("Should have raised error") - } - - e, ok := err.(*Error) - if !ok { - t.Fatalf("expected Error, got %#v", err) - } else if e.Code.Name() != "unique_violation" { - t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) - } + err := tx.QueryRow("insert into foo values (0), (0)").Scan(&v) + mustAs(t, err, pqerror.UniqueViolation) } // Test the QueryRow bug workarounds in stmt.exec() and simpleQuery() @@ -807,53 +736,25 @@ func TestQueryRowBugWorkaround(t *testing.T) { } var a string - err = db.QueryRow("INSERT INTO notnulltemp(a) values($1) RETURNING a", nil).Scan(&a) - if err == sql.ErrNoRows { - t.Fatalf("expected constraint violation error; got: %v", err) - } - pge, ok := err.(*Error) - if !ok { - t.Fatalf("expected *Error; got: %#v", err) - } - if pge.Code.Name() != "not_null_violation" { - t.Fatalf("expected not_null_violation; got: %s (%+v)", pge.Code.Name(), err) - } + err = db.QueryRow("insert into notnulltemp(a) values($1) returning a", nil).Scan(&a) + mustAs(t, err, pqerror.NotNullViolation) // Test workaround in simpleQuery() - tx, err := db.Begin() - if err != nil { - t.Fatalf("unexpected error %s in Begin", err) - } - defer tx.Rollback() + tx := pqtest.Begin(t, db) - _, err = tx.Exec("SET LOCAL check_function_bodies TO FALSE") - if err != nil { - t.Fatalf("could not disable check_function_bodies: %s", err) - } - _, err = tx.Exec(` - CREATE OR REPLACE FUNCTION bad_function() - RETURNS integer + pqtest.Exec(t, tx, `set local check_function_bodies to false`) + pqtest.Exec(t, tx, ` + create or replace function bad_function() + returns integer -- hack to prevent the function from being inlined - SET check_function_bodies TO TRUE - AS $$ - SELECT text 'bad' - $$ LANGUAGE sql + set check_function_bodies to true + as $$ + select text 'bad' + $$ language sql `) - if err != nil { - t.Fatalf("could not create function: %s", err) - } - err = tx.QueryRow("SELECT * FROM bad_function()").Scan(&a) - if err == nil { - t.Fatalf("expected error") - } - pge, ok = err.(*Error) - if !ok { - t.Fatalf("expected *Error; got: %#v", err) - } - if pge.Code.Name() != "invalid_function_definition" { - t.Fatalf("expected invalid_function_definition; got: %s (%+v)", pge.Code.Name(), err) - } + err = tx.QueryRow("select * from bad_function()").Scan(&a) + mustAs(t, err, pqerror.InvalidFunctionDefinition) err = tx.Rollback() if err != nil { @@ -885,13 +786,7 @@ func TestQueryRowBugWorkaround(t *testing.T) { if rows.Next() { t.Fatalf("unexpected row") } - pge, ok = rows.Err().(*Error) - if !ok { - t.Fatalf("expected *Error; got: %#v", err) - } - if pge.Code.Name() != "cardinality_violation" { - t.Fatalf("expected cardinality_violation; got: %s (%+v)", pge.Code.Name(), rows.Err()) - } + mustAs(t, rows.Err(), pqerror.CardinalityViolation) } func TestSimpleQuery(t *testing.T) { @@ -963,14 +858,10 @@ func TestParseErrorInExtendedQuery(t *testing.T) { t.Parallel() db := pqtest.MustDB(t) - _, err := db.Query("PARSE_ERROR $1", 1) - pqErr, _ := err.(*Error) - // Expecting a syntax error. - if err == nil || pqErr == nil || pqErr.Code != "42601" { - t.Fatalf("expected syntax error, got %s", err) - } + _, err := db.Query("parse_error $1", 1) + mustAs(t, err, pqerror.SyntaxError) - rows, err := db.Query("SELECT 1") + rows, err := db.Query("select 1") if err != nil { t.Fatal(err) } @@ -987,8 +878,7 @@ func TestReturning(t *testing.T) { t.Fatal(err) } - rows, err := db.Query("INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') " + - "RETURNING did;") + rows, err := db.Query("INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') RETURNING did;") if err != nil { t.Fatal(err) } @@ -1314,19 +1204,16 @@ func TestErrorClass(t *testing.T) { t.Parallel() db := pqtest.MustDB(t) - _, err := db.Query("SELECT int 'notint'") - if err == nil { + _, err := db.Query("select int 'notint'") + pqErr := As(err) + if pqErr == nil { t.Fatal("expected error") } - pge, ok := err.(*Error) - if !ok { - t.Fatalf("expected *pq.Error, got %#+v", err) + if pqErr.Code.Class() != "22" { + t.Fatalf("expected class 28, got %v", pqErr.Code.Class()) } - if pge.Code.Class() != "22" { - t.Fatalf("expected class 28, got %v", pge.Code.Class()) - } - if pge.Code.Class().Name() != "data_exception" { - t.Fatalf("expected data_exception, got %v", pge.Code.Class().Name()) + if pqErr.Code.Class().Name() != "data_exception" { + t.Fatalf("expected data_exception, got %v", pqErr.Code.Class().Name()) } } @@ -1534,31 +1421,26 @@ func TestConnPrepareContext(t *testing.T) { func TestStmtQueryContext(t *testing.T) { tests := []struct { - name string - ctx func() (context.Context, context.CancelFunc) - sql string - wantCancel bool + sql string + ctx func() (context.Context, context.CancelFunc) + wantErr string }{ - { - name: "context.WithTimeout exceeded", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(1)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 50*time.Millisecond) }, - sql: "select pg_sleep(1)", - wantCancel: true, + `pq: canceling statement due to user request (57014)`, }, - { - name: "context.WithTimeout", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(0.05)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Minute) }, - sql: "select pg_sleep(0.05)", - wantCancel: false, + ``, }, } for _, tt := range tests { tt := tt - t.Run(tt.name, func(t *testing.T) { + t.Run("", func(t *testing.T) { if !pqtest.Pgpool() { t.Parallel() } @@ -1566,20 +1448,15 @@ func TestStmtQueryContext(t *testing.T) { db := pqtest.MustDB(t) ctx, cancel := tt.ctx() - if cancel != nil { - defer cancel() - } + defer cancel() + stmt, err := db.PrepareContext(ctx, tt.sql) if err != nil { t.Fatal(err) } _, err = stmt.QueryContext(ctx) - pgErr := (*Error)(nil) - switch { - case (err != nil) != tt.wantCancel: - t.Fatalf("stmt.QueryContext() unexpected nil err got = %v, wantCancel = %v", err, tt.wantCancel) - case (err != nil && tt.wantCancel) && !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode): - t.Errorf("stmt.QueryContext() got = %v, wantCancel = %v", err.Error(), tt.wantCancel) + if !pqtest.ErrorContains(err, tt.wantErr) { + t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr) } }) } @@ -1587,31 +1464,26 @@ func TestStmtQueryContext(t *testing.T) { func TestStmtExecContext(t *testing.T) { tests := []struct { - name string - ctx func() (context.Context, context.CancelFunc) - sql string - wantCancel bool + sql string + ctx func() (context.Context, context.CancelFunc) + wantErr string }{ - { - name: "context.WithTimeout exceeded", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(1)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 50*time.Millisecond) }, - sql: "select pg_sleep(1)", - wantCancel: true, + `pq: canceling statement due to user request (57014)`, }, - { - name: "context.WithTimeout", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(0.05)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Minute) }, - sql: "select pg_sleep(0.05)", - wantCancel: false, + ``, }, } for _, tt := range tests { tt := tt - t.Run(tt.name, func(t *testing.T) { + t.Run("", func(t *testing.T) { if !pqtest.Pgpool() { t.Parallel() } @@ -1619,20 +1491,15 @@ func TestStmtExecContext(t *testing.T) { db := pqtest.MustDB(t) ctx, cancel := tt.ctx() - if cancel != nil { - defer cancel() - } + defer cancel() + stmt, err := db.PrepareContext(ctx, tt.sql) if err != nil { t.Fatal(err) } _, err = stmt.ExecContext(ctx) - pgErr := (*Error)(nil) - switch { - case (err != nil) != tt.wantCancel: - t.Fatalf("stmt.QueryContext() unexpected nil err got = %v, wantCancel = %v", err, tt.wantCancel) - case (err != nil && tt.wantCancel) && !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode): - t.Errorf("stmt.QueryContext() got = %v, wantCancel = %v", err.Error(), tt.wantCancel) + if !pqtest.ErrorContains(err, tt.wantErr) { + t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr) } }) } @@ -1712,11 +1579,8 @@ func TestContextCancelExec(t *testing.T) { defer time.AfterFunc(time.Millisecond*10, cancel).Stop() // Not canceled until after the exec has started. - if _, err := db.ExecContext(ctx, "select pg_sleep(1)"); err == nil { - t.Fatal("expected error") - } else if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("unexpected error: %s", err) - } + _, err := db.ExecContext(ctx, "select pg_sleep(1)") + mustAs(t, err, pqerror.QueryCanceled) // Context is already canceled, so error should come before execution. if _, err := db.ExecContext(ctx, "select pg_sleep(1)"); err == nil { @@ -1751,11 +1615,8 @@ func TestContextCancelQuery(t *testing.T) { defer time.AfterFunc(time.Millisecond*10, cancel).Stop() // Not canceled until after the exec has started. - if _, err := db.QueryContext(ctx, "select pg_sleep(1)"); err == nil { - t.Fatal("expected error") - } else if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("unexpected error: %s", err) - } + _, err := db.QueryContext(ctx, "select pg_sleep(1)") + mustAs(t, err, pqerror.QueryCanceled) // Context is already canceled, so error should come before execution. if _, err := db.QueryContext(ctx, "select pg_sleep(1)"); err == nil { @@ -1797,11 +1658,7 @@ func TestIssue617(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() _, err := db.QueryContext(ctx, `SELECT * FROM DOESNOTEXIST`) - pqErr, _ := err.(*Error) - // Expecting "pq: relation \"doesnotexist\" does not exist" error. - if err == nil || pqErr == nil || pqErr.Code != "42P01" { - t.Fatalf("expected undefined table error, got %v", err) - } + mustAs(t, err, pqerror.UndefinedTable) }() } @@ -1841,11 +1698,8 @@ func TestContextCancelBegin(t *testing.T) { defer time.AfterFunc(time.Millisecond*10, cancel).Stop() // Not canceled until after the exec has started. - if _, err := tx.Exec("select pg_sleep(1)"); err == nil { - t.Fatal("expected error") - } else if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("unexpected error: %s", err) - } + _, err = tx.Exec("select pg_sleep(1)") + mustAs(t, err, pqerror.QueryCanceled) // Transaction is canceled, so expect an error. if _, err := tx.Query("select pg_sleep(1)"); err == nil { @@ -1868,10 +1722,10 @@ func TestContextCancelBegin(t *testing.T) { cancel() if err != nil { t.Fatal(err) - } else if err, pgErr := tx.Rollback(), (*Error)(nil); err != nil && - !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) && - err != sql.ErrTxDone && err != driver.ErrBadConn && err != context.Canceled { - t.Fatal(err) + } + err = tx.Rollback() + if err != nil && err != sql.ErrTxDone && err != driver.ErrBadConn && err != context.Canceled { + mustAs(t, err, pqerror.QueryCanceled) } }() diff --git a/helper_test.go b/helper_test.go index f2224ca2b..902222af8 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1,6 +1,7 @@ package pq import ( + "slices" "testing" "github.com/lib/pq/internal/pqtest" @@ -10,22 +11,19 @@ import ( // Called for the side-effect of setting the environment. func init() { pqtest.DSN("") } -const cancelErrorCode pqerror.Code = "57014" - -// pqError converts an error to *pq.Error, calling t.Fatal() if the error is nil -// or if this fails. +// mustAs calls As(), calling t.Fatal() if the error is nil or if this fails. // // This should probably be in pqtest, but can't right now due to import cycles, // and using pq_test package requires some refactoring as it refers to // unexported symbols. -func pqError(t *testing.T, err error) *Error { +func mustAs(t *testing.T, err error, codes ...pqerror.Code) *Error { t.Helper() - if err == nil { - t.Fatalf("pqError: error is nil") + pqErr := As(err) + if pqErr == nil { + t.Fatalf("mustAs: not *pq.Error: %T", err) } - pqErr, ok := err.(*Error) - if !ok { - t.Fatalf("wrong error %T: %[1]s", err) + if len(codes) > 0 && !slices.Contains(codes, pqErr.Code) { + t.Fatalf("mustAs: wrong error %q (code not one of %s)", pqErr.Error(), codes) } return pqErr } diff --git a/issues_test.go b/issues_test.go index 9816531a9..08e6022c4 100644 --- a/issues_test.go +++ b/issues_test.go @@ -3,11 +3,11 @@ package pq import ( "context" "database/sql" - "errors" "testing" "time" "github.com/lib/pq/internal/pqtest" + "github.com/lib/pq/pqerror" ) // #1046: stmt.QueryRowContext doesn't respect canceled context @@ -32,11 +32,7 @@ func TestQueryRowContext(t *testing.T) { t.Logf("FAIL %s: query returned after context deadline: %v\n", t.Name(), since) t.Fail() } - if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Logf("ctx.Err(): [%T]%+v\n", ctx.Err(), ctx.Err()) - t.Logf("got err: [%T] %+v expected errCode: %v", err, err, cancelErrorCode) - t.Fail() - } + mustAs(t, err, pqerror.QueryCanceled) } // #1062: drivers.ErrBadConn returned for DB.QueryRowContext.Scan when context is cancelled @@ -55,10 +51,10 @@ func TestQueryRowContextBad(t *testing.T) { var v int err := row.Scan(&v) - if pgErr := (*Error)(nil); err != nil && - err != context.Canceled && - !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("Scan resulted in unexpected error %v for canceled QueryRowContext at attempt %d", err, i+1) + + // nil, context Canceled, and QueryCancelled are all fine. + if err != nil && err != context.Canceled { + mustAs(t, err, pqerror.QueryCanceled) } } } @@ -97,9 +93,8 @@ func TestQueryCancelRace(t *testing.T) { row := db.QueryRowContext(ctx, "select pg_sleep(0.5)") var pgSleepVoid string err := row.Scan(&pgSleepVoid) - if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("expected cancelled error; err=%#v", err) - } + + mustAs(t, err, pqerror.QueryCanceled) // get a connection: it must be a valid connIsValid(t, db) diff --git a/ssl_test.go b/ssl_test.go index 0eb5f17cb..07e548392 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -27,7 +27,7 @@ func startSSLTest(t *testing.T, user string) { wantErr = "internal_error" } _, err := pqtest.DB(t, "sslmode=disable user="+user) - pqErr := pqError(t, err) + pqErr := mustAs(t, err) if pqErr.Code.Name() != wantErr { t.Fatalf("wrong error code %q", pqErr.Code.Name()) }