Skip to content

Commit 11789ea

Browse files
Claude Botclaude
andcommitted
Add PostgreSQL error condition names and key/value parsing
- Add comprehensive mapping of PostgreSQL error codes to condition names using StaticStringMap - Add 'condition' field to PostgresError with human-readable condition names - Add 'key' and 'value' fields for unique constraint violations - Parse key/value from detail messages for 23505 (unique_violation) errors - Support all standard PostgreSQL error codes from documentation (200+ codes) - Use efficient StaticStringMap for O(1) error code lookups - Add unit tests for error condition mapping and key/value parsing Fixes #21698 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent c106820 commit 11789ea

File tree

3 files changed

+740
-0
lines changed

3 files changed

+740
-0
lines changed

src/sql/postgres/protocol/ErrorResponse.zig

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,298 @@ pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReade
2929

3030
pub const decode = DecoderWrap(ErrorResponse, decodeInternal).decode;
3131

32+
const ERROR_CODE_MAP = std.StaticStringMap([]const u8).initComptime(.{
33+
.{ "00000", "successful_completion" },
34+
.{ "01000", "warning" },
35+
.{ "0100C", "dynamic_result_sets_returned" },
36+
.{ "01008", "implicit_zero_bit_padding" },
37+
.{ "01003", "null_value_eliminated_in_set_function" },
38+
.{ "01007", "privilege_not_granted" },
39+
.{ "01006", "privilege_not_revoked" },
40+
.{ "01004", "string_data_right_truncation" },
41+
.{ "01P01", "deprecated_feature" },
42+
.{ "02000", "no_data" },
43+
.{ "02001", "no_additional_dynamic_result_sets_returned" },
44+
.{ "03000", "sql_statement_not_yet_complete" },
45+
.{ "08000", "connection_exception" },
46+
.{ "08003", "connection_does_not_exist" },
47+
.{ "08006", "connection_failure" },
48+
.{ "08001", "sqlclient_unable_to_establish_sqlconnection" },
49+
.{ "08004", "sqlserver_rejected_establishment_of_sqlconnection" },
50+
.{ "08007", "transaction_resolution_unknown" },
51+
.{ "08P01", "protocol_violation" },
52+
.{ "09000", "triggered_action_exception" },
53+
.{ "0A000", "feature_not_supported" },
54+
.{ "0B000", "invalid_transaction_initiation" },
55+
.{ "0F000", "locator_exception" },
56+
.{ "0F001", "invalid_locator_specification" },
57+
.{ "0L000", "invalid_grantor" },
58+
.{ "0LP01", "invalid_grant_operation" },
59+
.{ "0P000", "invalid_role_specification" },
60+
.{ "0Z000", "diagnostics_exception" },
61+
.{ "0Z002", "stacked_diagnostics_accessed_without_active_handler" },
62+
.{ "20000", "case_not_found" },
63+
.{ "21000", "cardinality_violation" },
64+
.{ "22000", "data_exception" },
65+
.{ "2202E", "array_subscript_error" },
66+
.{ "22021", "character_not_in_repertoire" },
67+
.{ "22008", "datetime_field_overflow" },
68+
.{ "22012", "division_by_zero" },
69+
.{ "22005", "error_in_assignment" },
70+
.{ "2200B", "escape_character_conflict" },
71+
.{ "22022", "indicator_overflow" },
72+
.{ "22015", "interval_field_overflow" },
73+
.{ "2201E", "invalid_argument_for_logarithm" },
74+
.{ "22014", "invalid_argument_for_ntile_function" },
75+
.{ "22016", "invalid_argument_for_nth_value_function" },
76+
.{ "2201F", "invalid_argument_for_power_function" },
77+
.{ "2201G", "invalid_argument_for_width_bucket_function" },
78+
.{ "22018", "invalid_character_value_for_cast" },
79+
.{ "22007", "invalid_datetime_format" },
80+
.{ "22019", "invalid_escape_character" },
81+
.{ "2200D", "invalid_escape_octet" },
82+
.{ "22025", "invalid_escape_sequence" },
83+
.{ "22P06", "nonstandard_use_of_escape_character" },
84+
.{ "22010", "invalid_indicator_parameter_value" },
85+
.{ "22023", "invalid_parameter_value" },
86+
.{ "2201B", "invalid_regular_expression" },
87+
.{ "2201W", "invalid_row_count_in_limit_clause" },
88+
.{ "2201X", "invalid_row_count_in_result_offset_clause" },
89+
.{ "2202H", "invalid_tablesample_argument" },
90+
.{ "2202G", "invalid_tablesample_repeat" },
91+
.{ "22009", "invalid_time_zone_displacement_value" },
92+
.{ "2200C", "invalid_use_of_escape_character" },
93+
.{ "2200G", "most_specific_type_mismatch" },
94+
.{ "22004", "null_value_not_allowed" },
95+
.{ "22002", "null_value_no_indicator_parameter" },
96+
.{ "22003", "numeric_value_out_of_range" },
97+
.{ "2200H", "sequence_generator_limit_exceeded" },
98+
.{ "22026", "string_data_length_mismatch" },
99+
.{ "22001", "string_data_right_truncation" },
100+
.{ "22011", "substring_error" },
101+
.{ "22027", "trim_error" },
102+
.{ "22024", "unterminated_c_string" },
103+
.{ "2200F", "zero_length_character_string" },
104+
.{ "22P01", "floating_point_exception" },
105+
.{ "22P02", "invalid_text_representation" },
106+
.{ "22P03", "invalid_binary_representation" },
107+
.{ "22P04", "bad_copy_file_format" },
108+
.{ "22P05", "untranslatable_character" },
109+
.{ "2200L", "not_an_xml_document" },
110+
.{ "2200M", "invalid_xml_document" },
111+
.{ "2200N", "invalid_xml_content" },
112+
.{ "2200S", "invalid_xml_comment" },
113+
.{ "2200T", "invalid_xml_processing_instruction" },
114+
.{ "23000", "integrity_constraint_violation" },
115+
.{ "23001", "restrict_violation" },
116+
.{ "23502", "not_null_violation" },
117+
.{ "23503", "foreign_key_violation" },
118+
.{ "23505", "unique_violation" },
119+
.{ "23514", "check_violation" },
120+
.{ "23P01", "exclusion_violation" },
121+
.{ "24000", "invalid_cursor_state" },
122+
.{ "25000", "invalid_transaction_state" },
123+
.{ "25001", "active_sql_transaction" },
124+
.{ "25002", "branch_transaction_already_active" },
125+
.{ "25008", "held_cursor_requires_same_isolation_level" },
126+
.{ "25003", "inappropriate_access_mode_for_branch_transaction" },
127+
.{ "25004", "inappropriate_isolation_level_for_branch_transaction" },
128+
.{ "25005", "no_active_sql_transaction_for_branch_transaction" },
129+
.{ "25006", "read_only_sql_transaction" },
130+
.{ "25007", "schema_and_data_statement_mixing_not_supported" },
131+
.{ "25P01", "no_active_sql_transaction" },
132+
.{ "25P02", "in_failed_sql_transaction" },
133+
.{ "25P03", "idle_in_transaction_session_timeout" },
134+
.{ "26000", "invalid_sql_statement_name" },
135+
.{ "27000", "triggered_data_change_violation" },
136+
.{ "28000", "invalid_authorization_specification" },
137+
.{ "28P01", "invalid_password" },
138+
.{ "2B000", "dependent_privilege_descriptors_still_exist" },
139+
.{ "2BP01", "dependent_objects_still_exist" },
140+
.{ "2D000", "invalid_transaction_termination" },
141+
.{ "2F000", "sql_routine_exception" },
142+
.{ "2F005", "function_executed_no_return_statement" },
143+
.{ "2F002", "modifying_sql_data_not_permitted" },
144+
.{ "2F003", "prohibited_sql_statement_attempted" },
145+
.{ "2F004", "reading_sql_data_not_permitted" },
146+
.{ "34000", "invalid_cursor_name" },
147+
.{ "38000", "external_routine_exception" },
148+
.{ "38001", "containing_sql_not_permitted" },
149+
.{ "38002", "modifying_sql_data_not_permitted" },
150+
.{ "38003", "prohibited_sql_statement_attempted" },
151+
.{ "38004", "reading_sql_data_not_permitted" },
152+
.{ "39000", "external_routine_invocation_exception" },
153+
.{ "39001", "invalid_sqlstate_returned" },
154+
.{ "39004", "null_value_not_allowed" },
155+
.{ "39P01", "trigger_protocol_violated" },
156+
.{ "39P02", "srf_protocol_violated" },
157+
.{ "39P03", "event_trigger_protocol_violated" },
158+
.{ "3B000", "savepoint_exception" },
159+
.{ "3B001", "invalid_savepoint_specification" },
160+
.{ "3D000", "invalid_catalog_name" },
161+
.{ "3F000", "invalid_schema_name" },
162+
.{ "40000", "transaction_rollback" },
163+
.{ "40002", "transaction_integrity_constraint_violation" },
164+
.{ "40001", "serialization_failure" },
165+
.{ "40003", "statement_completion_unknown" },
166+
.{ "40P01", "deadlock_detected" },
167+
.{ "42000", "syntax_error_or_access_rule_violation" },
168+
.{ "42601", "syntax_error" },
169+
.{ "42501", "insufficient_privilege" },
170+
.{ "42846", "cannot_coerce" },
171+
.{ "42803", "grouping_error" },
172+
.{ "42P20", "windowing_error" },
173+
.{ "42P19", "invalid_recursion" },
174+
.{ "42830", "invalid_foreign_key" },
175+
.{ "42602", "invalid_name" },
176+
.{ "42622", "name_too_long" },
177+
.{ "42939", "reserved_name" },
178+
.{ "42804", "datatype_mismatch" },
179+
.{ "42P18", "indeterminate_datatype" },
180+
.{ "42P21", "collation_mismatch" },
181+
.{ "42P22", "indeterminate_collation" },
182+
.{ "42809", "wrong_object_type" },
183+
.{ "428C9", "generated_always" },
184+
.{ "42703", "undefined_column" },
185+
.{ "42883", "undefined_function" },
186+
.{ "42P01", "undefined_table" },
187+
.{ "42P02", "undefined_parameter" },
188+
.{ "42704", "undefined_object" },
189+
.{ "42701", "duplicate_column" },
190+
.{ "42P03", "duplicate_cursor" },
191+
.{ "42P04", "duplicate_database" },
192+
.{ "42723", "duplicate_function" },
193+
.{ "42P05", "duplicate_prepared_statement" },
194+
.{ "42P06", "duplicate_schema" },
195+
.{ "42P07", "duplicate_table" },
196+
.{ "42712", "duplicate_alias" },
197+
.{ "42710", "duplicate_object" },
198+
.{ "42702", "ambiguous_column" },
199+
.{ "42725", "ambiguous_function" },
200+
.{ "42P08", "ambiguous_parameter" },
201+
.{ "42P09", "ambiguous_alias" },
202+
.{ "42P10", "invalid_column_reference" },
203+
.{ "42611", "invalid_column_definition" },
204+
.{ "42P11", "invalid_cursor_definition" },
205+
.{ "42P12", "invalid_database_definition" },
206+
.{ "42P13", "invalid_function_definition" },
207+
.{ "42P14", "invalid_prepared_statement_definition" },
208+
.{ "42P15", "invalid_schema_definition" },
209+
.{ "42P16", "invalid_table_definition" },
210+
.{ "42P17", "invalid_object_definition" },
211+
.{ "44000", "with_check_option_violation" },
212+
.{ "53000", "insufficient_resources" },
213+
.{ "53100", "disk_full" },
214+
.{ "53200", "out_of_memory" },
215+
.{ "53300", "too_many_connections" },
216+
.{ "53400", "configuration_limit_exceeded" },
217+
.{ "54000", "program_limit_exceeded" },
218+
.{ "54001", "statement_too_complex" },
219+
.{ "54011", "too_many_columns" },
220+
.{ "54023", "too_many_arguments" },
221+
.{ "55000", "object_not_in_prerequisite_state" },
222+
.{ "55006", "object_in_use" },
223+
.{ "55P02", "cant_change_runtime_param" },
224+
.{ "55P03", "lock_not_available" },
225+
.{ "55P04", "unsafe_new_enum_value_usage" },
226+
.{ "57000", "operator_intervention" },
227+
.{ "57014", "query_canceled" },
228+
.{ "57P01", "admin_shutdown" },
229+
.{ "57P02", "crash_shutdown" },
230+
.{ "57P03", "cannot_connect_now" },
231+
.{ "57P04", "database_dropped" },
232+
.{ "58000", "system_error" },
233+
.{ "58030", "io_error" },
234+
.{ "58P01", "undefined_file" },
235+
.{ "58P02", "duplicate_file" },
236+
.{ "72000", "snapshot_too_old" },
237+
.{ "F0000", "config_file_error" },
238+
.{ "F0001", "lock_file_exists" },
239+
.{ "HV000", "fdw_error" },
240+
.{ "HV005", "fdw_column_name_not_found" },
241+
.{ "HV002", "fdw_dynamic_parameter_value_needed" },
242+
.{ "HV010", "fdw_function_sequence_error" },
243+
.{ "HV021", "fdw_inconsistent_descriptor_information" },
244+
.{ "HV024", "fdw_invalid_attribute_value" },
245+
.{ "HV007", "fdw_invalid_column_name" },
246+
.{ "HV008", "fdw_invalid_column_number" },
247+
.{ "HV004", "fdw_invalid_data_type" },
248+
.{ "HV006", "fdw_invalid_data_type_descriptors" },
249+
.{ "HV091", "fdw_invalid_descriptor_field_identifier" },
250+
.{ "HV00B", "fdw_invalid_handle" },
251+
.{ "HV00C", "fdw_invalid_option_index" },
252+
.{ "HV00D", "fdw_invalid_option_name" },
253+
.{ "HV090", "fdw_invalid_string_length_or_buffer_length" },
254+
.{ "HV00A", "fdw_invalid_string_format" },
255+
.{ "HV009", "fdw_invalid_use_of_null_pointer" },
256+
.{ "HV014", "fdw_too_many_handles" },
257+
.{ "HV001", "fdw_out_of_memory" },
258+
.{ "HV00P", "fdw_no_schemas" },
259+
.{ "HV00J", "fdw_option_name_not_found" },
260+
.{ "HV00K", "fdw_reply_handle" },
261+
.{ "HV00Q", "fdw_schema_not_found" },
262+
.{ "HV00R", "fdw_table_not_found" },
263+
.{ "HV00L", "fdw_unable_to_create_execution" },
264+
.{ "HV00M", "fdw_unable_to_create_reply" },
265+
.{ "HV00N", "fdw_unable_to_establish_connection" },
266+
.{ "P0000", "plpgsql_error" },
267+
.{ "P0001", "raise_exception" },
268+
.{ "P0002", "no_data_found" },
269+
.{ "P0003", "too_many_rows" },
270+
.{ "P0004", "assert_failure" },
271+
.{ "XX000", "internal_error" },
272+
.{ "XX001", "data_corrupted" },
273+
.{ "XX002", "index_corrupted" },
274+
});
275+
276+
fn getConditionName(error_code: String) ?[]const u8 {
277+
if (error_code.isEmpty()) return null;
278+
279+
const code_str = error_code.toUTF8WithoutRef(bun.default_allocator);
280+
defer code_str.deinit();
281+
282+
return ERROR_CODE_MAP.get(code_str.slice());
283+
}
284+
285+
const KeyValuePair = struct {
286+
key: []const u8,
287+
value: []const u8,
288+
};
289+
290+
fn parseKeyValueFromDetail(detail: String, allocator: std.mem.Allocator) ?KeyValuePair {
291+
if (detail.isEmpty()) return null;
292+
293+
const detail_str = detail.toUTF8WithoutRef(allocator);
294+
defer detail_str.deinit();
295+
const detail_slice = detail_str.slice();
296+
297+
// Parse format: "Key (column_name)=(value) already exists."
298+
if (std.mem.indexOf(u8, detail_slice, "Key (")) |start| {
299+
const after_key = start + 5; // "Key (".len
300+
if (std.mem.indexOf(u8, detail_slice[after_key..], ")=(")) |end_key_relative| {
301+
const end_key = after_key + end_key_relative;
302+
const key = detail_slice[after_key..end_key];
303+
304+
const value_start = end_key + 3; // ")=(".len
305+
if (std.mem.indexOf(u8, detail_slice[value_start..], ") ")) |end_value_relative| {
306+
const end_value = value_start + end_value_relative;
307+
const value = detail_slice[value_start..end_value];
308+
309+
// Allocate and copy the strings
310+
const key_copy = allocator.dupe(u8, key) catch return null;
311+
const value_copy = allocator.dupe(u8, value) catch {
312+
allocator.free(key_copy);
313+
return null;
314+
};
315+
316+
return KeyValuePair{ .key = key_copy, .value = value_copy };
317+
}
318+
}
319+
}
320+
321+
return null;
322+
}
323+
32324
pub fn toJS(this: ErrorResponse, globalObject: *jsc.JSGlobalObject) JSValue {
33325
var b = bun.StringBuilder{};
34326
defer b.deinit(bun.default_allocator);
@@ -106,6 +398,17 @@ pub fn toJS(this: ErrorResponse, globalObject: *jsc.JSGlobalObject) JSValue {
106398
}
107399
}
108400

401+
// Parse key/value from detail for unique constraint violations
402+
var key_value_data: ?KeyValuePair = null;
403+
defer if (key_value_data) |kv| {
404+
bun.default_allocator.free(kv.key);
405+
bun.default_allocator.free(kv.value);
406+
};
407+
408+
if (code.eqlComptime("23505") and !detail.isEmpty()) {
409+
key_value_data = parseKeyValueFromDetail(detail, bun.default_allocator);
410+
}
411+
109412
const possible_fields = .{
110413
.{ "detail", detail, void },
111414
.{ "hint", hint, void },
@@ -143,6 +446,19 @@ pub fn toJS(this: ErrorResponse, globalObject: *jsc.JSGlobalObject) JSValue {
143446
}
144447
}
145448

449+
// Add condition name if we have an error code
450+
if (!code.isEmpty()) {
451+
if (getConditionName(code)) |condition_name| {
452+
err.put(globalObject, jsc.ZigString.static("condition"), jsc.ZigString.init(condition_name).toJS(globalObject));
453+
}
454+
}
455+
456+
// Add key and value fields for unique constraint violations
457+
if (key_value_data) |kv| {
458+
err.put(globalObject, jsc.ZigString.static("key"), jsc.ZigString.init(kv.key).toJS(globalObject));
459+
err.put(globalObject, jsc.ZigString.static("value"), jsc.ZigString.init(kv.value).toJS(globalObject));
460+
}
461+
146462
return err;
147463
}
148464

0 commit comments

Comments
 (0)