classifyErrorCode returns the most appropriate PostgreSQL SQLSTATE for a DuckDB error. Transaction conflicts get 40001 (serialization_failure), which signals PG-aware clients to retry. Query cancellations get 57014. Remaining errors are classified by DuckDB's exception-type prefix; the prefix is the
(err error)
| 67 | // errors are classified by DuckDB's exception-type prefix; the prefix is the |
| 68 | // only signal the Go driver exposes today, so we parse the message string. |
| 69 | func classifyErrorCode(err error) string { |
| 70 | // Wire-side SQLSTATE: 57014 is correct for any cancellation regardless of |
| 71 | // whether the caller drove it. Use isQueryCancelled here, not the |
| 72 | // clientConn-aware variant — classifyErrorCode is called from contexts |
| 73 | // that don't have a *clientConn (e.g., direct test fixtures). |
| 74 | if isQueryCancelled(err) { |
| 75 | return "57014" |
| 76 | } |
| 77 | if isDuckLakeTransactionConflict(err) { |
| 78 | return "40001" // serialization_failure — client should retry |
| 79 | } |
| 80 | |
| 81 | // Workers reach the control plane over Arrow Flight SQL, so DuckDB errors |
| 82 | // arrive wrapped as "flight execute update: rpc error: code = X desc = <msg>". |
| 83 | // Unwrap to recover the underlying DuckDB exception message so the prefix |
| 84 | // classifiers below apply; otherwise every DuckLake worker error |
| 85 | // would fall through to XX000 and the client would see the raw rpc string. |
| 86 | msg := unwrapFlightError(err.Error()) |
| 87 | switch { |
| 88 | case strings.HasPrefix(msg, "Catalog Error:"): |
| 89 | return catalogErrorCode(msg) |
| 90 | case strings.HasPrefix(msg, "Binder Error:"): |
| 91 | return binderErrorCode(msg) |
| 92 | case strings.HasPrefix(msg, "Parser Error:"): |
| 93 | return "42601" // syntax_error |
| 94 | case strings.HasPrefix(msg, "Conversion Error:"): |
| 95 | return conversionErrorCode(msg) |
| 96 | case strings.HasPrefix(msg, "Out of Range Error:"): |
| 97 | return "22003" // numeric_value_out_of_range |
| 98 | case strings.HasPrefix(msg, "Constraint Error:"): |
| 99 | return constraintErrorCode(msg) |
| 100 | case strings.HasPrefix(msg, "Permission Error:"): |
| 101 | return "42501" // insufficient_privilege |
| 102 | case strings.HasPrefix(msg, "Not implemented Error:"), |
| 103 | strings.HasPrefix(msg, "Invalid Input Error: Not implemented"): |
| 104 | return "0A000" // feature_not_supported |
| 105 | case strings.HasPrefix(msg, "Transaction Error:"), |
| 106 | strings.HasPrefix(msg, "TransactionContext Error:"): |
| 107 | return "25000" // invalid_transaction_state — DuckDB emits both prefixes |
| 108 | case strings.HasPrefix(msg, "Dependency Error:"): |
| 109 | return "2BP01" // dependent_objects_still_exist |
| 110 | } |
| 111 | // Unknown error class — no DuckDB prefix matched. These are |
| 112 | // typically infra issues (gRPC failures, IO errors, internal panics) |
| 113 | // rather than user input issues. Classify as XX000 (internal_error) |
| 114 | // so isUserQueryError correctly routes them to the system-error log |
| 115 | // path. If a future DuckDB error needs to land in a user class, add |
| 116 | // a prefix branch above instead of moving the fallback. |
| 117 | return "XX000" |
| 118 | } |
| 119 | |
| 120 | // transformErrorSQLState returns the SQLSTATE for a transpiler-detected error. |
| 121 | // A transform may attach an explicit code (e.g. 0A000 for an unsupported |