diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index 5b25402ebbe..98ff688ee27 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -18,6 +18,9 @@ # src/pl/tcl/pltclerrcodes.h # the same, for PL/Tcl # +# src/interfaces/libpq/sqlstate_names.h +# a lookup table mapping SQLSTATE codes to symbolic names for libpq +# # doc/src/sgml/errcodes-table.sgml # a SGML table of error codes for inclusion in the documentation # diff --git a/src/backend/utils/generate-sqlstate-names.pl b/src/backend/utils/generate-sqlstate-names.pl new file mode 100644 index 00000000000..99e7a9b36a6 --- /dev/null +++ b/src/backend/utils/generate-sqlstate-names.pl @@ -0,0 +1,100 @@ +#!/usr/bin/perl +# +# Generate the sqlstate_names.h header from errcodes.txt +# Copyright (c) 2000-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; +use Getopt::Long; + +my $outfile = ''; + +GetOptions('outfile=s' => \$outfile) or die "$0: wrong arguments"; + +open my $errcodes, '<', $ARGV[0] + or die "$0: could not open input file '$ARGV[0]': $!\n"; + +my $outfh; +if ($outfile) +{ + open $outfh, '>', $outfile + or die "$0: could not open output file '$outfile': $!\n"; +} +else +{ + $outfh = *STDOUT; +} + +my @entries; + +while (<$errcodes>) +{ + chomp; + + # Skip comments and empty lines + next if /^#/; + next if /^\s*$/; + + # Skip section headers + next if /^Section:/; + + die "unable to parse errcodes.txt" + unless /^([^\s]{5})\s+[EWS]\s+([^\s]+)/; + + push @entries, [$1, $2]; +} + +close $errcodes; + +# Sort by SQLSTATE +@entries = sort { $a->[0] cmp $b->[0] } @entries; + +print $outfh <<'HEADER'; +/* autogenerated from src/backend/utils/errcodes.txt, do not edit */ +#ifndef SQLSTATE_NAMES_H +#define SQLSTATE_NAMES_H + +#include + +typedef struct +{ + const char sqlstate[6]; + const char *symbolic_name; +} SqlstateNameEntry; + +static const SqlstateNameEntry sqlstate_name_table[] = { +HEADER + +for my $e (@entries) +{ + print $outfh "\t{\"$e->[0]\", \"$e->[1]\"},\n"; +} + +print $outfh <<'FOOTER'; +}; + +static inline const char * +sqlstate_to_symbolic_name(const char *sqlstate) +{ + int lo = 0; + int hi = sizeof(sqlstate_name_table) / sizeof(sqlstate_name_table[0]) - 1; + + while (lo <= hi) + { + int mid = (lo + hi) / 2; + int cmp = strncmp(sqlstate, sqlstate_name_table[mid].sqlstate, 5); + + if (cmp < 0) + hi = mid - 1; + else if (cmp > 0) + lo = mid + 1; + else + return sqlstate_name_table[mid].symbolic_name; + } + return NULL; +} + +#endif /* SQLSTATE_NAMES_H */ +FOOTER + +close $outfh if ($outfile); diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 0963995eed4..af06d6733e6 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -146,6 +146,12 @@ endif fe-connect.o: fe-connect.c $(top_builddir)/src/port/pg_config_paths.h fe-misc.o: fe-misc.c $(top_builddir)/src/port/pg_config_paths.h +# generate sqlstate_names.h from src/backend/utils/errcodes.txt +sqlstate_names.h: $(top_srcdir)/src/backend/utils/errcodes.txt $(top_srcdir)/src/backend/utils/generate-sqlstate-names.pl + $(PERL) $(top_srcdir)/src/backend/utils/generate-sqlstate-names.pl --outfile $@ $< + +fe-protocol3.o: fe-protocol3.c sqlstate_names.h + $(top_builddir)/src/port/pg_config_paths.h: $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h @@ -188,5 +194,6 @@ clean distclean: clean-lib $(MAKE) -C test $@ rm -rf tmp_check rm -f $(OBJS) $(OBJS_SHLIB) $(OBJS_STATIC) pthread.h libpq-refs-stamp + rm -f sqlstate_names.h # Might be left over from a Win32 client-only build rm -f pg_config_paths.h diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 8c1fda5caf0..04a3c9a9ece 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -28,6 +28,7 @@ #include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" +#include "sqlstate_names.h" #include "mb/pg_wchar.h" #include "port/pg_bswap.h" @@ -1080,7 +1081,14 @@ pqBuildErrorMessage3(PQExpBuffer msg, const PGresult *res, { val = PQresultErrorField(res, PG_DIAG_SQLSTATE); if (val) - appendPQExpBuffer(msg, "%s: ", val); + { + const char *symbolic = sqlstate_to_symbolic_name(val); + + if (symbolic) + appendPQExpBuffer(msg, "%s (%s): ", val, symbolic); + else + appendPQExpBuffer(msg, "%s: ", val); + } } val = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); if (val) diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index b0ae72167a1..d847d5c4414 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -42,6 +42,15 @@ export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) +gen_sqlstate_names = files('../../backend/utils/generate-sqlstate-names.pl') +sqlstate_names_h = custom_target('sqlstate_names', + input: ['../../backend/utils/errcodes.txt'], + output: ['sqlstate_names.h'], + command: [perl, gen_sqlstate_names, '--outfile', '@OUTPUT@', '@INPUT0@'], +) +generated_sources += sqlstate_names_h +libpq_sources += sqlstate_names_h + # port needs to be in include path due to pthread-win32.h libpq_inc = include_directories('.', '../../port') libpq_c_args = ['-DSO_MAJOR_VERSION=5']