On 10/29/2025 6:53 AM, Euler Taveira wrote:
On Tue, Oct 28, 2025, at 1:51 PM, Álvaro Herrera wrote:
On 2025-Oct-27, Euler Taveira wrote:

On Mon, Oct 27, 2025, at 2:58 PM, Bryan Green wrote:
     Thanks for even glancing at this.  I did not add any regression
tests because the output goes to the server log and not the client.

Since Michael said WIN32-specific tests and mentioned log pattern, he is
referring to TAP tests. You can add src/test/modules/test_backtrace that
exercises this code path.

Hmm, are we really sure this is necessary?


Good question. We are testing an external API. Maybe a test in this thread is
enough to convince someone that the code is correct.

Right, attached is v2 with TAP tests added.  The test checks whether
postgres.pdb exists on disk to figure out what kind of output to expect,
then verifies the actual backtrace format matches.  This ought to catch
the case where the PDB is there but DbgHelp can't load it for some reason, which seems like the most likely failure mode.

It also runs a bunch of errors in quick succession to make sure we don't
crash or anything.  (Can't really detect memory leaks without external
tools, so didn't try.)>>> I didn't test your patch but I'm wondering if we could add an
abstraction here.  I mean invent pg_backtrace() and
pg_backtrace_symbols() that maps to the current functions (Unix-like).

Do we really need this?  I don't think we're going to add support for
backtraces anywhere else any time soon, so this looks premature.  What
other programs do you think we have where this would be useful?  I have
a really hard time imagining that things like psql and pg_dump would be
improved by having backtrace-reporting support.  And if we have a single
place in the code using a facility, then ISTM the platform-specific code
can live there with no damage.


It was just a suggestion; not a requirement. As you said it would avoid rework
in the future if or when someone decides to use this code in other parts of the
software. I wouldn't propose this change if I knew it was complicated to have a
simple and clean abstraction.


I am not convinced that we need an abstraction considering our backtrace logic for both platforms is in one place at the moment and not spread throughout the source. I have a hard time imagining this being used anywhere but where it currently is... I am more than happy to do as the community wishes though.

The latest patch has tap tests now and I look forward to any guidance the community wishes to provide.

Bryan Green
From 1aed9a7572bcdfbd789a69eea5d4a7c7a47b300a Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Tue, 21 Oct 2025 19:31:29 -0500
Subject: [PATCH v2] Add Windows support for backtrace_functions.

backtrace_functions has been Unix-only up to now, because we relied on
glibc's backtrace() or similar platform facilities.  Windows doesn't have
anything equivalent in its standard C library, but it does have the DbgHelp
API, which can do the same thing if you ask it nicely.

The tricky bit is that DbgHelp needs to be initialized with SymInitialize()
before you can use it, and that's a fairly expensive operation.  We don't
want to do that every time we generate a backtrace.  Fortunately, it turns
out that we can initialize once per process and reuse the handle, which is
safe since we're holding an exclusive lock during error reporting anyway.
So the code just initializes lazily on first use.

If SymInitialize fails, we don't consider that an error; we just silently
decline to generate backtraces.  This seems reasonable since backtraces are
a debugging aid, not critical to operation.  It also matches the behavior
on platforms where backtrace() isn't available.

Symbol resolution quality depends on whether PDB files are present.  If they
are, DbgHelp can give us source file paths and line numbers, which is great.
If not, it can still give us function names by reading the export table, and
that turns out to be good enough because postgres.exe exports thousands of
functions.  (You get export symbols on Windows whether you like it or not,
unless you go out of your way to suppress them.  Might as well take advantage
of that.)  Fully stripped binaries would only show addresses, but that's not
a scenario that applies to Postgres, so we don't worry about it.

The TAP test verifies that symbol resolution works correctly in both the
with-PDB and without-PDB cases.  We have to use TAP because backtraces go
to the server log, not to psql.  The test figures out which case should
apply by checking whether postgres.pdb exists on disk, then parses the
backtrace output to see what we actually got.  If those don't match, that's
a bug.  This should catch the case where the PDB exists but DbgHelp fails
to load it, which seems like the most likely way this could break.

The test also verifies that we can generate a bunch of backtraces in quick
succession without crashing, which is really just a basic sanity check.  It
doesn't attempt to detect memory leaks, since that would require external
tools we don't want to depend on.

Author: Bryan Green
---
 src/backend/meson.build                       |   5 +
 src/backend/utils/error/elog.c                | 174 +++++++
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_backtrace/Makefile      |  33 ++
 src/test/modules/test_backtrace/README        | 224 +++++++++
 src/test/modules/test_backtrace/meson.build   |  13 +
 .../test_backtrace/t/t_windows_backtrace.pl   | 428 ++++++++++++++++++
 .../test_backtrace/test_backtrace--1.0.sql    |  66 +++
 .../test_backtrace/test_backtrace.control     |   5 +
 9 files changed, 949 insertions(+)
 create mode 100644 src/test/modules/test_backtrace/Makefile
 create mode 100644 src/test/modules/test_backtrace/README
 create mode 100644 src/test/modules/test_backtrace/meson.build
 create mode 100644 src/test/modules/test_backtrace/t/t_windows_backtrace.pl
 create mode 100644 src/test/modules/test_backtrace/test_backtrace--1.0.sql
 create mode 100644 src/test/modules/test_backtrace/test_backtrace.control

diff --git a/src/backend/meson.build b/src/backend/meson.build
index b831a54165..eeb69c4079 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -1,6 +1,11 @@
 # Copyright (c) 2022-2025, PostgreSQL Global Development Group
 
 backend_build_deps = [backend_code]
+
+if host_system == 'windows' and cc.get_id() == 'msvc'
+  backend_build_deps += cc.find_library('dbghelp')
+endif
+
 backend_sources = []
 backend_link_with = [pgport_srv, common_srv]
 
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 29643c5143..fc421ce444 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -140,6 +140,13 @@ static void write_syslog(int level, const char *line);
 static void write_eventlog(int level, const char *line, int len);
 #endif
 
+#ifdef _MSC_VER
+#include <windows.h>
+#include <dbghelp.h>
+static bool win32_backtrace_symbols_initialized = false;
+static HANDLE win32_backtrace_process = NULL;
+#endif
+
 /* We provide a small stack of ErrorData records for re-entrant cases */
 #define ERRORDATA_STACK_SIZE  5
 
@@ -1116,6 +1123,18 @@ errbacktrace(void)
        return 0;
 }
 
+#ifdef _MSC_VER
+/*
+ * Cleanup function for DbgHelp resources.
+ * Called via on_proc_exit() to release resources allocated by SymInitialize().
+ */
+static void
+win32_backtrace_cleanup(int code, Datum arg)
+{
+       SymCleanup(win32_backtrace_process);
+}
+#endif
+
 /*
  * Compute backtrace data and add it to the supplied ErrorData.  num_skip
  * specifies how many inner frames to skip.  Use this to avoid showing the
@@ -1147,6 +1166,161 @@ set_backtrace(ErrorData *edata, int num_skip)
                        appendStringInfoString(&errtrace,
                                                                   
"insufficient memory for backtrace generation");
        }
+#elif defined(_MSC_VER)
+       {
+               void       *stack[100];
+               DWORD           frames;
+               DWORD           i;
+               wchar_t         buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * 
sizeof(wchar_t)];
+               PSYMBOL_INFOW symbol;
+               char       *utf8_buffer;
+               int                     utf8_len;
+
+               if (!win32_backtrace_symbols_initialized)
+               {
+                       win32_backtrace_process = GetCurrentProcess();
+
+                       SymSetOptions(SYMOPT_UNDNAME |
+                                                 SYMOPT_DEFERRED_LOADS |
+                                                 SYMOPT_LOAD_LINES |
+                                                 SYMOPT_FAIL_CRITICAL_ERRORS);
+
+                       if (SymInitialize(win32_backtrace_process, NULL, TRUE))
+                       {
+                               win32_backtrace_symbols_initialized = true;
+                               on_proc_exit(win32_backtrace_cleanup, 0);
+                       }
+                       else
+                       {
+                               DWORD           error = GetLastError();
+
+                               elog(WARNING, "SymInitialize failed with error 
%lu", error);
+                       }
+               }
+
+               frames = CaptureStackBackTrace(num_skip, lengthof(stack), 
stack, NULL);
+
+               if (frames == 0)
+               {
+                       appendStringInfoString(&errtrace, "\nNo stack frames 
captured");
+                       edata->backtrace = errtrace.data;
+                       return;
+               }
+
+               symbol = (PSYMBOL_INFOW) buffer;
+               symbol    ->MaxNameLen = MAX_SYM_NAME;
+               symbol    ->SizeOfStruct = sizeof(SYMBOL_INFOW);
+
+               for (i = 0; i < frames; i++)
+               {
+                       DWORD64         address = (DWORD64) (stack[i]);
+                       DWORD64         displacement = 0;
+                       BOOL            sym_result;
+
+                       sym_result = SymFromAddrW(win32_backtrace_process,
+                                                                         
address,
+                                                                         
&displacement,
+                                                                         
symbol);
+
+                       if (sym_result)
+                       {
+                               IMAGEHLP_LINEW64 line;
+                               DWORD           line_displacement = 0;
+
+                               line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
+
+                               if 
(SymGetLineFromAddrW64(win32_backtrace_process,
+                                                                               
  address,
+                                                                               
  &line_displacement,
+                                                                               
  &line))
+                               {
+                                       /* Convert symbol name to UTF-8 */
+                                       utf8_len = WideCharToMultiByte(CP_UTF8, 
0, symbol->Name, -1,
+                                                                               
                   NULL, 0, NULL, NULL);
+                                       if (utf8_len > 0)
+                                       {
+                                               char       *filename_utf8;
+                                               int                     
filename_len;
+
+                                               utf8_buffer = palloc(utf8_len);
+                                               WideCharToMultiByte(CP_UTF8, 0, 
symbol->Name, -1,
+                                                                               
        utf8_buffer, utf8_len, NULL, NULL);
+
+                                               /* Convert file name to UTF-8 */
+                                               filename_len = 
WideCharToMultiByte(CP_UTF8, 0,
+                                                                               
                                   line.FileName, -1,
+                                                                               
                                   NULL, 0, NULL, NULL);
+                                               if (filename_len > 0)
+                                               {
+                                                       filename_utf8 = 
palloc(filename_len);
+                                                       
WideCharToMultiByte(CP_UTF8, 0, line.FileName, -1,
+                                                                               
                filename_utf8, filename_len,
+                                                                               
                NULL, NULL);
+
+                                                       
appendStringInfo(&errtrace,
+                                                                               
         "\n%s+0x%llx [%s:%lu]",
+                                                                               
         utf8_buffer,
+                                                                               
         (unsigned long long) displacement,
+                                                                               
         filename_utf8,
+                                                                               
         (unsigned long) line.LineNumber);
+
+                                                       pfree(filename_utf8);
+                                               }
+                                               else
+                                               {
+                                                       
appendStringInfo(&errtrace,
+                                                                               
         "\n%s+0x%llx [0x%llx]",
+                                                                               
         utf8_buffer,
+                                                                               
         (unsigned long long) displacement,
+                                                                               
         (unsigned long long) address);
+                                               }
+
+                                               pfree(utf8_buffer);
+                                       }
+                                       else
+                                       {
+                                               /* Conversion failed, use 
address only */
+                                               appendStringInfo(&errtrace,
+                                                                               
 "\n[0x%llx]",
+                                                                               
 (unsigned long long) address);
+                                       }
+                               }
+                               else
+                               {
+                                       /* No line info, convert symbol name 
only */
+                                       utf8_len = WideCharToMultiByte(CP_UTF8, 
0, symbol->Name, -1,
+                                                                               
                   NULL, 0, NULL, NULL);
+                                       if (utf8_len > 0)
+                                       {
+                                               utf8_buffer = palloc(utf8_len);
+                                               WideCharToMultiByte(CP_UTF8, 0, 
symbol->Name, -1,
+                                                                               
        utf8_buffer, utf8_len, NULL, NULL);
+
+                                               appendStringInfo(&errtrace,
+                                                                               
 "\n%s+0x%llx [0x%llx]",
+                                                                               
 utf8_buffer,
+                                                                               
 (unsigned long long) displacement,
+                                                                               
 (unsigned long long) address);
+
+                                               pfree(utf8_buffer);
+                                       }
+                                       else
+                                       {
+                                               /* Conversion failed, use 
address only */
+                                               appendStringInfo(&errtrace,
+                                                                               
 "\n[0x%llx]",
+                                                                               
 (unsigned long long) address);
+                                       }
+                               }
+                       }
+                       else
+                       {
+                               appendStringInfo(&errtrace,
+                                                                "\n[0x%llx]",
+                                                                (unsigned long 
long) address);
+                       }
+               }
+       }
 #else
        appendStringInfoString(&errtrace,
                                                   "backtrace generation is not 
supported by this installation");
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 14fc761c4c..ccb63f2b57 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('plsample')
 subdir('spgist_name_ops')
 subdir('ssl_passphrase_callback')
 subdir('test_aio')
+subdir('test_backtrace')
 subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
diff --git a/src/test/modules/test_backtrace/Makefile 
b/src/test/modules/test_backtrace/Makefile
new file mode 100644
index 0000000000..3e3112cd74
--- /dev/null
+++ b/src/test/modules/test_backtrace/Makefile
@@ -0,0 +1,33 @@
+# src/test/modules/test_backtrace/Makefile
+#
+# Makefile for Windows backtrace testing module
+
+MODULE_big = test_backtrace
+OBJS = test_backtrace.o
+
+EXTENSION = test_backtrace
+DATA = test_backtrace--1.0.sql
+
+# Only TAP tests - no SQL regression tests since backtraces
+# go to server logs, not to client output
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_backtrace
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+# Platform-specific test handling
+ifeq ($(PORTNAME),win32)
+    # Run all tests on Windows
+    PROVE_FLAGS += -v
+else
+    # Skip tests on non-Windows platforms
+    TAP_TESTS = 0
+endif
diff --git a/src/test/modules/test_backtrace/README 
b/src/test/modules/test_backtrace/README
new file mode 100644
index 0000000000..7388320ad9
--- /dev/null
+++ b/src/test/modules/test_backtrace/README
@@ -0,0 +1,224 @@
+================================================================================
+                         Windows Backtrace Tests
+================================================================================
+
+TAP tests for the Windows backtrace implementation, which uses the DbgHelp API
+to capture and format stack traces.
+
+--------------------------------------------------------------------------------
+Why TAP Tests?
+--------------------------------------------------------------------------------
+
+Backtraces appear in the server log, not in psql output.  So we can't use SQL
+tests to validate them; we need TAP tests that can read the actual log files.
+
+--------------------------------------------------------------------------------
+Test File
+--------------------------------------------------------------------------------
+
+t_windows_backtrace.pl
+
+  Tests backtrace generation in two scenarios:
+
+  (1) WITH PDB FILE: postgres.pdb exists alongside postgres.exe
+      Expected: Function names, source files, line numbers, addresses
+      Validates: DbgHelp loads the PDB and resolves full symbols
+
+  (2) WITHOUT PDB FILE: postgres.pdb is absent
+      Expected: Function names and addresses (from export table)
+      Validates: We can still get useful backtraces without debug info
+
+  The test figures out which scenario applies by checking whether postgres.pdb
+  exists, then validates that the actual backtrace format matches expectations.
+  If the PDB is present but we don't get source files, that's a bug.
+
+  What gets tested:
+
+    - Basic functionality (backtraces appear in logs)
+    - Symbol resolution (correct format for scenario)
+    - Various error types (div-by-zero, constraints, etc)
+    - PL/pgSQL and trigger integration
+    - Stability (20 rapid errors don't crash anything)
+    - DbgHelp initialization doesn't fail
+
+  What doesn't get tested:
+
+    - Memory leaks (would need Dr. Memory or similar)
+    - Symbol servers (would need network setup)
+    - Stripped binaries (not relevant for Postgres)
+
+--------------------------------------------------------------------------------
+Running the Tests
+--------------------------------------------------------------------------------
+
+Prerequisites:
+
+  - Windows (test skips on other platforms)
+  - MSVC build (needs DbgHelp)
+  - Perl with PostgreSQL::Test modules
+
+From the PostgreSQL build directory:
+
+  meson test --suite test_backtrace --verbose
+
+Or with prove:
+
+  set PERL5LIB=C:\path\to\postgres\src\test\perl
+  prove src/test/modules/test_backtrace/t/t_windows_backtrace.pl
+
+To test both scenarios:
+
+  (1) WITH PDB: Just run the test normally
+      - PDB should be in the same directory as postgres.exe
+      - Test expects to find source file information
+
+  (2) WITHOUT PDB: Delete the PDB file and re-run
+      - del build_dir\tmp_install\...\bin\postgres.pdb
+      - Test expects export symbols only
+
+--------------------------------------------------------------------------------
+Expected Output
+--------------------------------------------------------------------------------
+
+With PDB:
+
+  PDB file found: C:\...\postgres.pdb
+  EXPECTED: Scenario 1 (full PDB symbols)
+  ACTUAL: Scenario 1 (found source files and symbols)
+  ok - Scenario matches expectation: Scenario 1
+
+  ERROR:  division by zero
+  BACKTRACE:
+  int4div+0x2a [C:\postgres\src\backend\utils\adt\int.c:841] [0x00007FF6...]
+  ExecInterpExpr+0x1b3 [C:\postgres\src\backend\executor\execExprInterp.c:2345]
+  ...
+
+Without PDB:
+
+  PDB file not found: C:\...\postgres.pdb
+  EXPECTED: Scenario 2 (export symbols only)
+  ACTUAL: Scenario 2 (found symbols but no source files)
+  ok - Scenario matches expectation: Scenario 2
+
+  ERROR:  division by zero
+  BACKTRACE:
+  int4div+0x2a [0x00007FF6...]
+  ExecInterpExpr+0x1b3 [0x00007FF6...]
+  ...
+
+Note: Postgres exports ~11,000 functions, so even without a PDB, you get
+function names.  Fully stripped binaries would only show addresses, but
+that's not a scenario we care about for Postgres.
+
+Failure (PDB exists but doesn't load):
+
+  PDB file found: C:\...\postgres.pdb
+  EXPECTED: Scenario 1 (full PDB symbols)
+  ACTUAL: Scenario 2 (found symbols but no source files)
+  not ok - PDB file exists but symbols not loading!
+
+  This means DbgHelp couldn't load the PDB.  Possible causes: corrupted PDB,
+  mismatched PDB (from different build), or DbgHelp initialization failed.
+
+--------------------------------------------------------------------------------
+How It Works
+--------------------------------------------------------------------------------
+
+The test validates expected vs actual:
+
+  1. Check if postgres.pdb exists on disk
+     -> If yes, expect full PDB symbols
+     -> If no, expect export symbols only
+
+  2. Parse the backtrace output
+     -> If source files present, got PDB symbols
+     -> If no source files, got exports only
+
+  3. Compare expected to actual
+     -> Pass if they match
+     -> Fail if they don't (indicates a problem)
+
+This catches the case where the PDB exists but DbgHelp fails to load it,
+which is the most likely failure mode.
+
+--------------------------------------------------------------------------------
+Configuration
+--------------------------------------------------------------------------------
+
+The test configures the server with:
+
+  backtrace_functions = 'int4div,int4in,ExecInterpExpr'
+  log_error_verbosity = verbose
+  logging_collector = on
+  log_destination = 'stderr'
+  log_min_messages = error
+
+Nothing fancy.  Just enough to generate backtraces and make them easy to find
+in the logs.
+
+--------------------------------------------------------------------------------
+Limitations
+--------------------------------------------------------------------------------
+
+This test verifies basic functionality.  It does not:
+
+  - Detect memory leaks (would need Dr. Memory, ASAN, or similar)
+  - Test symbol server scenarios (would need network setup and config)
+  - Validate symbol accuracy in detail (just checks format)
+  - Test performance or memory usage
+  - Validate path remapping (future work)
+
+The test ensures the feature works and doesn't crash.  That's about it.
+
+--------------------------------------------------------------------------------
+Troubleshooting
+--------------------------------------------------------------------------------
+
+"PDB file exists but symbols not loading!"
+
+  The PDB is there but DbgHelp couldn't use it.
+
+  Check:
+    - Is the PDB corrupted?
+    - Does the PDB match the executable? (same build)
+    - Are there SymInitialize errors in the log?
+
+"Scenario mismatch" (other than PDB not loading)
+
+  Something weird happened.  Look at the test output to see what was expected
+  vs what was found, and figure out what's going on.
+
+No backtraces at all
+
+  Check:
+    - Is backtrace_functions configured?
+    - Is logging_collector enabled?
+    - Are there SymInitialize failures in the log?
+
+Only addresses, no function names (even without PDB)
+
+  This would be very strange, since Postgres exports thousands of functions.
+  DbgHelp should be able to get them from the export table.  Check that
+  postgres.exe was linked normally.
+
+Test hangs
+
+  Probably a logging issue.  Check that logging_collector is working and
+  log files are appearing in the data directory.
+
+--------------------------------------------------------------------------------
+Symbol Servers
+--------------------------------------------------------------------------------
+
+This test doesn't try to exercise symbol server functionality.  It just checks
+whether a local PDB file gets used.  Symbol servers are a deployment concern,
+not a functionality test.
+
+For deployments, you'd typically either:
+  - Ship PDB files alongside executables (development/staging)
+  - Don't ship PDB files (production, smaller footprint)
+
+In the latter case, you still get useful backtraces from the export table.
+Whether that's sufficient depends on your debugging needs.
+
+================================================================================
diff --git a/src/test/modules/test_backtrace/meson.build 
b/src/test/modules/test_backtrace/meson.build
new file mode 100644
index 0000000000..b8b1b8e198
--- /dev/null
+++ b/src/test/modules/test_backtrace/meson.build
@@ -0,0 +1,13 @@
+# TAP tests - only run on Windows
+if host_system == 'windows'
+  tests += {
+    'name': 'test_backtrace',
+    'sd': meson.current_source_dir(),
+    'bd': meson.current_build_dir(),
+    'tap': {
+      'tests': [
+        't/t_windows_backtrace.pl',
+      ],
+    },
+  }
+endif
diff --git a/src/test/modules/test_backtrace/t/t_windows_backtrace.pl 
b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
new file mode 100644
index 0000000000..7609da99eb
--- /dev/null
+++ b/src/test/modules/test_backtrace/t/t_windows_backtrace.pl
@@ -0,0 +1,428 @@
+#!/usr/bin/perl
+
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Test Windows backtrace generation using DbgHelp API.
+#
+# The main thing we need to verify is that the DbgHelp integration actually
+# works, and that we can resolve symbols both with and without PDB files
+# present.  We don't try to test symbol server scenarios or anything fancy;
+# just check that if you have a PDB, you get source file info, and if you
+# don't, you still get function names from the export table (which works
+# because postgres.exe exports thousands of functions).
+#
+# The test automatically detects which situation applies by checking whether
+# postgres.pdb exists alongside postgres.exe.  This avoids the need to know
+# how the executable was built or what build type we're testing.  If the PDB
+# is there but we don't get source info, that's a bug.  If it's not there
+# but we somehow get source info anyway, that's weird and worth investigating.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Skip if not Windows
+if ($^O ne 'MSWin32')
+{
+       plan skip_all => 'Windows-specific backtrace tests';
+}
+
+# Initialize node
+my $node = PostgreSQL::Test::Cluster->new('backtrace_test');
+$node->init;
+
+# Configure for detailed logging with backtraces
+$node->append_conf('postgresql.conf', qq{
+backtrace_functions = 'int4div,int4in,ExecInterpExpr'
+log_error_verbosity = verbose
+logging_collector = on
+log_destination = 'stderr'
+log_min_messages = error
+});
+
+$node->start;
+
+# Helper to get recent log content.
+# Backtraces go to the server log, not to psql output, so we need to read
+# the actual log files to validate anything.
+sub get_recent_log_content
+{
+       my $log_dir = $node->data_dir . '/log';
+       my @log_files = glob("$log_dir/*.log $log_dir/*.csv");
+
+       # Get the most recent log file
+       my $latest_log = (sort { -M $a <=> -M $b } @log_files)[0];
+
+       return '' unless defined $latest_log && -f $latest_log;
+
+       my $content = '';
+       open(my $fh, '<', $latest_log) or return '';
+       {
+               local $/;
+               $content = <$fh>;
+       }
+       close($fh);
+
+       return $content;
+}
+
+###############################################################################
+# First, verify basic functionality and figure out what scenario we're in.
+#
+# We trigger an error and check that (a) it actually generates a backtrace,
+# and (b) we can tell from the backtrace format whether we have PDB symbols
+# or just exports.  Then we compare that to what we should have based on
+# whether postgres.pdb exists on disk.
+###############################################################################
+
+note('');
+note('=== PART 1: Basic Error Tests & Scenario Detection ===');
+
+### Test 1: Division by zero
+my ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;");
+ok($ret != 0, 'division by zero error occurred');
+like($stderr, qr/division by zero/i, 'division by zero error message in psql');
+
+sleep 1;
+my $log_content = get_recent_log_content();
+like($log_content, qr/ERROR:.*division by zero/i, 'error logged to server 
log');
+like($log_content, qr/BACKTRACE:/i, 'BACKTRACE header in log');
+
+### Test 2: Detect scenario and validate it matches expectations
+#
+# The backtrace format tells us what DbgHelp actually gave us.  Source file
+# paths mean we got PDB symbols; lack thereof means export symbols only.
+#
+# We then check whether postgres.pdb exists on disk.  If it does, we should
+# have gotten PDB symbols; if not, we should have gotten exports only.
+# Mismatches indicate a problem with DbgHelp initialization or PDB loading.
+my $has_source_files = ($log_content =~ /\[[\w:\\\/]+\.\w+:\d+\]/);  # 
[file.c:123]
+my $has_symbols = ($log_content =~ /\w+\+0x[0-9a-fA-F]+/);           # 
function+0xABC
+my $has_addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/);          # 
[0xABCDEF]
+
+ok($has_symbols || $has_addresses, 'backtrace has valid format');
+
+# Determine EXPECTED scenario based on PDB file existence.
+#
+# We need to find where postgres.exe actually lives.  The TAP test framework
+# creates a temporary install, and we can find the bin directory by looking
+# at the parent of the data directory.  If that doesn't work, search PATH.
+# (This is a bit ugly but there's no direct way to ask the test node for
+# its bin directory.)
+my $datadir = $node->data_dir;
+my $postgres_exe;
+
+# Try to find the bin directory relative to data directory
+# Typical structure: .../tmp_install/PORT_XXX/data and 
.../tmp_install/PORT_XXX/bin
+if ($datadir =~ /^(.+)[\\\/][^\\\/]+$/)
+{
+       my $base = $1;  # Get parent directory
+       $postgres_exe = "$base/bin/postgres.exe";
+}
+
+# Fallback: try to construct from test environment
+if (!defined($postgres_exe) || !-f $postgres_exe)
+{
+       # Try using PATH - the test sets up PATH to include the bin directory
+       my $path_dirs = $ENV{PATH};
+       foreach my $dir (split(/;/, $path_dirs))
+       {
+               my $candidate = "$dir/postgres.exe";
+               if (-f $candidate)
+               {
+                       $postgres_exe = $candidate;
+                       last;
+               }
+       }
+}
+
+# If still not found, just use the command name and note that we couldn't find 
it
+if (!defined($postgres_exe))
+{
+       $postgres_exe = 'postgres.exe';
+}
+
+my $postgres_pdb = $postgres_exe;
+$postgres_pdb =~ s/\.exe$/.pdb/i;
+
+my $expected_scenario;
+if (-f $postgres_pdb)
+{
+       $expected_scenario = 1;  # PDB exists, we SHOULD have full symbols
+       note("PDB file found: $postgres_pdb");
+       note("EXPECTED: Scenario 1 (full PDB symbols)");
+}
+else
+{
+       $expected_scenario = 2;  # No PDB, we SHOULD have export symbols only
+       note("PDB file not found: $postgres_pdb");
+       note("EXPECTED: Scenario 2 (export symbols only)");
+}
+
+# Determine ACTUAL scenario from log output
+my $actual_scenario;
+if ($has_source_files && $has_symbols)
+{
+       $actual_scenario = 1;  # Full PDB symbols
+       note('ACTUAL: Scenario 1 (found source files and symbols)');
+}
+elsif ($has_symbols && !$has_source_files)
+{
+       $actual_scenario = 2;  # Export symbols only
+       note('ACTUAL: Scenario 2 (found symbols but no source files)');
+}
+else
+{
+       $actual_scenario = 0;  # Unknown/invalid
+       fail('Unable to determine scenario - PostgreSQL should always have 
export symbols');
+       note('Expected either Scenario 1 (with PDB) or Scenario 2 (without 
PDB)');
+}
+
+# CRITICAL TEST: Validate actual matches expected.
+#
+# This is the main point of the test.  We need to verify that DbgHelp is
+# actually loading symbols correctly.  If the PDB exists but we don't get
+# source files, that's a bug.  If the PDB doesn't exist but we somehow get
+# source files anyway, that's bizarre and worth investigating.
+if ($actual_scenario == $expected_scenario)
+{
+       pass("Scenario matches expectation: Scenario $actual_scenario");
+       note('');
+       if ($actual_scenario == 1) {
+               note('*** SCENARIO 1: Full PDB symbols (build WITH .pdb file) 
***');
+               note('Build type: Release/Debug/DebugOptimized WITH .pdb file');
+               note('Format: function+offset [file.c:line] [0xaddress]');
+       }
+       elsif ($actual_scenario == 2) {
+               note('*** SCENARIO 2: Export symbols only (build WITHOUT .pdb 
file) ***');
+               note('Build type: Release WITHOUT .pdb file');
+               note('Format: function+offset [0xaddress]');
+       }
+       note('');
+}
+elsif ($expected_scenario == 1 && $actual_scenario == 2)
+{
+       fail('PDB file exists but symbols not loading!');
+       note("PDB file found at: $postgres_pdb");
+       note('Expected: Full PDB symbols with source files');
+       note('Actual: Only export symbols (no source files)');
+       note('This indicates PDB is not being loaded by DbgHelp');
+}
+elsif ($expected_scenario == 2 && $actual_scenario == 1)
+{
+       fail('Found PDB symbols but PDB file does not exist!');
+       note("PDB file not found at: $postgres_pdb");
+       note('Expected: Export symbols only');
+       note('Actual: Full PDB symbols with source files');
+       note('This is unexpected - where are the symbols coming from?');
+}
+else
+{
+       fail("Scenario mismatch: expected $expected_scenario, got 
$actual_scenario");
+}
+
+my $scenario = $actual_scenario;
+
+###############################################################################
+# Now validate the backtrace format matches what we expect for this scenario.
+#
+# If we have PDB symbols, check for function names, source files, and 
addresses.
+# If we only have exports, check that source files are absent (as expected).
+# The point is to verify that the format is sane, not just that it exists.
+###############################################################################
+
+note('');
+note('=== PART 2: Format Validation ===');
+
+if ($scenario == 1)
+{
+       # Scenario 1: Full PDB symbols
+       like($log_content, qr/\w+\+0x[0-9a-fA-F]+/,
+               'Scenario 1: function+offset format present');
+
+       like($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/,
+               'Scenario 1: source files and line numbers present');
+
+       like($log_content, qr/\[0x[0-9a-fA-F]+\]/,
+               'Scenario 1: addresses present');
+
+       # Extract example paths to show in test output
+       my @example_paths = ($log_content =~ /\[([^\]]+\.\w+:\d+)\]/g);
+
+       if (@example_paths)
+       {
+               pass('Scenario 1: backtrace includes source file paths');
+               note("Example path: $example_paths[0]");
+       }
+       else
+       {
+               fail('Scenario 1: no source file paths found');
+       }
+}
+elsif ($scenario == 2)
+{
+       # Scenario 2: Export symbols only
+       like($log_content, qr/\w+\+0x[0-9a-fA-F]+/,
+               'Scenario 2: function+offset format present');
+
+       like($log_content, qr/\[0x[0-9a-fA-F]+\]/,
+               'Scenario 2: addresses present');
+
+       unlike($log_content, qr/\[[\w:\\\/]+\.\w+:\d+\]/,
+               'Scenario 2: no source files (expected without PDB)');
+}
+
+###############################################################################
+# Test that backtraces work for various types of errors.
+#
+# The backtrace mechanism should work regardless of what triggered the error.
+# Try a few different error types to make sure we're not somehow dependent on
+# the error path.  Also check that PL/pgSQL and triggers work, since those go
+# through different code paths.
+###############################################################################
+
+note('');
+note('=== PART 3: Error Scenario Coverage ===');
+
+### Test: Type conversion error
+($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 
'invalid'::integer;");
+ok($ret != 0, 'type conversion error occurred');
+like($stderr, qr/invalid input syntax/i, 'type conversion error message');
+
+### Test: Constraint violation
+$node->safe_psql('postgres', "CREATE TABLE test_table (id integer PRIMARY 
KEY);");
+($ret, $stdout, $stderr) = $node->psql('postgres',
+       "INSERT INTO test_table VALUES (1), (1);");
+ok($ret != 0, 'constraint violation occurred');
+like($stderr, qr/(duplicate key|unique constraint)/i, 'constraint violation 
message');
+
+### Test: PL/pgSQL nested function
+$node->safe_psql('postgres', q{
+       CREATE FUNCTION nested_func() RETURNS void AS $$
+       BEGIN
+               PERFORM 1/0;
+       END;
+       $$ LANGUAGE plpgsql;
+});
+
+($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT nested_func();");
+ok($ret != 0, 'nested function error occurred');
+
+sleep 1;
+$log_content = get_recent_log_content();
+my @addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g);
+my $frame_count = scalar @addresses;
+ok($frame_count >= 3,
+       "PL/pgSQL error has deeper stack (found $frame_count frames)");
+
+### Test: Trigger error
+$node->safe_psql('postgres', q{
+       CREATE TABLE trigger_test (val integer);
+
+       CREATE FUNCTION trigger_func() RETURNS trigger AS $$
+       BEGIN
+               PERFORM 1/0;
+               RETURN NEW;
+       END;
+       $$ LANGUAGE plpgsql;
+
+       CREATE TRIGGER test_trigger BEFORE INSERT ON trigger_test
+               FOR EACH ROW EXECUTE FUNCTION trigger_func();
+});
+
+($ret, $stdout, $stderr) = $node->psql('postgres',
+       "INSERT INTO trigger_test VALUES (1);");
+ok($ret != 0, 'trigger error occurred');
+like($stderr, qr/division by zero/i, 'trigger error message');
+
+###############################################################################
+# Verify that repeated backtrace generation doesn't cause problems.
+#
+# We don't have a good way to detect memory leaks in this test, but we can
+# at least check that the server doesn't crash or start spewing errors after
+# we've generated a bunch of backtraces.  Also verify that DbgHelp doesn't
+# log any initialization failures.
+###############################################################################
+
+note('');
+note('=== PART 4: Stability Tests ===');
+
+### Test: Multiple errors don't crash the server
+my $error_count = 0;
+for my $i (1..20)
+{
+       ($ret, $stdout, $stderr) = $node->psql('postgres', "SELECT 1/0;");
+       $error_count++ if ($ret != 0);
+}
+
+is($error_count, 20, 'all 20 rapid errors occurred (DbgHelp stable)');
+
+sleep 2;
+$log_content = get_recent_log_content();
+@addresses = ($log_content =~ /\[0x[0-9a-fA-F]+\]/g);
+ok(scalar(@addresses) >= 20,
+       'multiple rapid errors produced backtraces (' . scalar(@addresses) . ' 
addresses found)');
+
+### Test: No SymInitialize failures
+unlike($log_content, qr/SymInitialize.*failed/i,
+       'no SymInitialize failures in log');
+
+### Test: Repeated errors in same session
+$node->safe_psql('postgres', q{
+       DO $$
+       BEGIN
+               FOR i IN 1..10 LOOP
+                       BEGIN
+                               EXECUTE 'SELECT 1/0';
+                       EXCEPTION
+                               WHEN division_by_zero THEN
+                                       NULL;  -- Swallow the error
+                       END;
+               END LOOP;
+       END $$;
+});
+
+pass('repeated errors in same session did not crash');
+
+###############################################################################
+# Summary
+###############################################################################
+
+note('');
+note('=== TEST SUMMARY ===');
+note("Scenario: $scenario");
+note('');
+
+if ($scenario == 1) {
+       note('BUILD TYPE: Release/Debug/DebugOptimized WITH .pdb file');
+       note('');
+       note('Validated:');
+       note('  ✓ Function names with offsets');
+       note('  ✓ Source files and line numbers');
+       note('  ✓ Memory addresses');
+       note('  ✓ Stack depth (shallow and nested)');
+       note('  ✓ Multiple error scenarios');
+       note('  ✓ Stability (20 rapid errors, no crashes)');
+       note('  ✓ No DbgHelp initialization failures');
+}
+elsif ($scenario == 2) {
+       note('BUILD TYPE: Release WITHOUT .pdb file');
+       note('');
+       note('Validated:');
+       note('  ✓ Function names with offsets');
+       note('  ✓ Memory addresses');
+       note('  ✗ No source files (expected - no PDB)');
+       note('  ✓ Stack depth (shallow and nested)');
+       note('  ✓ Multiple error scenarios');
+       note('  ✓ Stability (20 rapid errors, no crashes)');
+       note('  ✓ No DbgHelp initialization failures');
+}
+note('====================');
+note('');
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_backtrace/test_backtrace--1.0.sql 
b/src/test/modules/test_backtrace/test_backtrace--1.0.sql
new file mode 100644
index 0000000000..f2e614a18d
--- /dev/null
+++ b/src/test/modules/test_backtrace/test_backtrace--1.0.sql
@@ -0,0 +1,66 @@
+/* src/test/modules/test_backtrace/test_backtrace--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_backtrace" to load this file. \quit
+
+--
+-- Test functions for Windows backtrace functionality
+--
+
+-- Function that triggers a division by zero error
+CREATE FUNCTION test_backtrace_div_by_zero()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM 1/0;
+END;
+$$;
+
+-- Function that triggers a type conversion error
+CREATE FUNCTION test_backtrace_type_error()
+RETURNS integer
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN 'not a number'::integer;
+END;
+$$;
+
+-- Nested function calls to test call stack depth
+CREATE FUNCTION test_backtrace_level3()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM 1/0;
+END;
+$$;
+
+CREATE FUNCTION test_backtrace_level2()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM test_backtrace_level3();
+END;
+$$;
+
+CREATE FUNCTION test_backtrace_level1()
+RETURNS void
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    PERFORM test_backtrace_level2();
+END;
+$$;
+
+-- Test array bounds error
+CREATE FUNCTION test_backtrace_array_bounds()
+RETURNS integer
+LANGUAGE plpgsql
+AS $$
+BEGIN
+    RETURN ('{1,2,3}'::int[])[10];
+END;
+$$;
diff --git a/src/test/modules/test_backtrace/test_backtrace.control 
b/src/test/modules/test_backtrace/test_backtrace.control
new file mode 100644
index 0000000000..ac27b28287
--- /dev/null
+++ b/src/test/modules/test_backtrace/test_backtrace.control
@@ -0,0 +1,5 @@
+# test_backtrace extension
+comment = 'Test module for Windows backtrace functionality'
+default_version = '1.0'
+module_pathname = '$libdir/test_backtrace'
+relocatable = true
-- 
2.46.0.windows.1

Reply via email to