From 70af8e8218544c874ce378084044b8e3b65f1042 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Wed, 18 Feb 2026 10:52:58 +0100 Subject: [PATCH 1/4] Cover the build_exclude_extension_string with conditional compilation. It is just the fact that --exclude-extension has been introduced in Postgres 18. --- src/spock_sync.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spock_sync.c b/src/spock_sync.c index b3919ef1..31ddb96e 100644 --- a/src/spock_sync.c +++ b/src/spock_sync.c @@ -166,7 +166,7 @@ get_pg_executable(char *cmdname, char *cmdbuf) * The sub->skip_schema typically doesn't include our globally-skipped objects. * So, don't care about duplicates. */ - +#if PG_VERSION_NUM >= 180000 static List * build_exclude_extension_string(void) { @@ -184,7 +184,7 @@ build_exclude_extension_string(void) } return lst; } - +#endif static List * build_exclude_schema_string(SpockSubscription *sub) { From 2a064d4082c6eb4f87fb675d052768dc0c58a123 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Wed, 18 Feb 2026 11:10:57 +0100 Subject: [PATCH 2/4] Remove Windows platform support code Drop Win32-specific code paths including exec_cmd_win32(), GetTempPath() temp directory resolution, Windows argument quoting, and related type definitions. Spock targets POSIX-only environments. --- include/spock_sync.h | 4 - src/spock.c | 16 -- src/spock_sync.c | 336 +---------------------------------- utils/pgindent/typedefs.list | 7 - 4 files changed, 1 insertion(+), 362 deletions(-) diff --git a/include/spock_sync.h b/include/spock_sync.h index 2169c07a..b1cf40e2 100644 --- a/include/spock_sync.h +++ b/include/spock_sync.h @@ -102,8 +102,4 @@ extern bool wait_for_sync_status_change(Oid subid, const char *nspname, extern void truncate_table(char *nspname, char *relname); extern List *get_subscription_tables(Oid subid); -#ifdef WIN32 -extern void QuoteWindowsArgv(StringInfo cmdline, const char *argv[]); -#endif - #endif /* SPOCK_SYNC_H */ diff --git a/src/spock.c b/src/spock.c index f23c9f94..40785084 100644 --- a/src/spock.c +++ b/src/spock.c @@ -769,28 +769,12 @@ spock_temp_directory_assing_hook(const char *newval, void *extra) } else { -#ifndef WIN32 const char *tmpdir = getenv("TMPDIR"); if (!tmpdir) tmpdir = "/tmp"; -#else - char tmpdir[MAXPGPATH]; - int ret; - - ret = GetTempPath(MAXPGPATH, tmpdir); - if (ret == 0 || ret > MAXPGPATH) - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("could not locate temporary directory: %s\n", - !ret ? strerror(errno) : ""))); - return false; - } -#endif spock_temp_directory = strdup(tmpdir); - } if (spock_temp_directory == NULL) diff --git a/src/spock_sync.c b/src/spock_sync.c index 31ddb96e..6b351aea 100644 --- a/src/spock_sync.c +++ b/src/spock_sync.c @@ -13,12 +13,7 @@ #include "postgres.h" #include - -#ifdef WIN32 -#include -#else #include -#endif #include "libpq-fe.h" @@ -89,14 +84,10 @@ PGDLLEXPORT void spock_sync_main(Datum main_arg); static SpockSyncWorker *MySyncWorker = NULL; -#ifdef WIN32 -static int exec_cmd_win32(const char *cmd, char *cmdargv[]); -#endif - /* * Run a command and wait for it to exit, then return its exit code - * in the same format as waitpid() including on Windows. + * in the same format as waitpid(). * * Does not elog(ERROR). * @@ -122,7 +113,6 @@ exec_cmd(const char *cmd, char *cmdargv[]) fflush(stdout); fflush(stderr); -#ifndef WIN32 if ((pid = fork()) == 0) { if (execv(cmd, cmdargv) < 0) @@ -136,9 +126,6 @@ exec_cmd(const char *cmd, char *cmdargv[]) if (waitpid(pid, &stat, 0) != pid) stat = -1; -#else - stat = exec_cmd_win32(cmd, cmdargv); -#endif return stat; } @@ -2292,324 +2279,3 @@ truncate_table(char *nspname, char *relname) CommandCounterIncrement(); } - - -/* - * exec_cmd support for win32 - */ -#ifdef WIN32 -/* - * Return formatted message from GetLastError() in a palloc'd string in the - * current memory context, or a copy of a constant generic error string if - * there's no recorded error state. - */ -static char * -PglGetLastWin32Error(void) -{ - LPVOID lpMsgBuf; - DWORD dw = GetLastError(); - char *pgstr = NULL; - - if (dw != ERROR_SUCCESS) - { - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); - pgstr = pstrdup((LPTSTR) lpMsgBuf); - LocalFree(lpMsgBuf); - } - else - { - pgstr = pstrdup("Unknown error or no recent error"); - } - - return pgstr; -} - - -/* - * See https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way - * for the utterly putrid way Windows handles command line arguments, and the insane lack of any inverse - * form of the CommandLineToArgvW function in the win32 API. - */ -static void -QuoteWindowsArgvElement(StringInfo cmdline, const char *arg, bool force) -{ - if (!force && *arg != '\0' - && strchr(arg, ' ') == NULL - && strchr(arg, '\t') == NULL - && strchr(arg, '\n') == NULL - && strchr(arg, '\v') == NULL - && strchr(arg, '"') == NULL) - { - appendStringInfoString(cmdline, arg); - } - else - { - const char *it; - - /* Begin quoted argument */ - appendStringInfoChar(cmdline, '"'); - - /* - * In terms of the algorithm described in CommandLineToArgvW's - * documentation we are now "in quotes". - */ - - for (it = arg; *it != '\0'; it++) - { - unsigned int NumberBackslashes = 0; - - /* - * Accumulate runs of backslashes. They may or may not have - * special meaning depending on what follows them. - */ - while (*it != '\0' && *it == '\\') - { - ++it; - ++NumberBackslashes; - } - - if (*it == '\0') - { - /* - * Handle command line arguments ending with or consisting - * only of backslashes. Particularly important for Windows, - * given its backslash paths. - * - * We want NumberBackSlashes * 2 backslashes here to prevent - * the final backslash from escaping the quote we'll append at - * the end of the argument. - */ - for (; NumberBackslashes > 0; NumberBackslashes--) - appendStringInfoString(cmdline, "\\\\"); - break; - } - else if (*it == '"') - { - /* - * Escape all accumulated backslashes, then append escaped - * quotation mark. - * - * We want NumberBackSlashes * 2 + 1 backslashes to prevent - * the backslashes from escaping the backslash we have to - * append to escape the quote char that's part of the argument - * itself. - */ - for (; NumberBackslashes > 0; NumberBackslashes--) - appendStringInfoString(cmdline, "\\\\"); - appendStringInfoString(cmdline, "\\\""); - } - else - { - /* - * A series of backslashes followed by something other than a - * double quote is not special to the CommandLineToArgvW - * parser in MSVCRT and must be appended literally. - */ - for (; NumberBackslashes > 0; NumberBackslashes--) - appendStringInfoChar(cmdline, '\\'); - /* Finally any normal char */ - appendStringInfoChar(cmdline, *it); - } - } - - /* End quoted argument */ - appendStringInfoChar(cmdline, '"'); - - /* - * In terms of the algorithm described in CommandLineToArgvW's - * documentation we are now "not in quotes". - */ - } -} - -/* - * Turn an execv-style argument vector into something that Win32's - * CommandLineToArgvW will parse back into the original argument - * vector. - * - * You'd think this would be part of the win32 API. But no... - * - * (This should arguably be part of libpq_fe.c, but I didn't want to expand our - * abuse of PqExpBuffer.) - */ -static void -QuoteWindowsArgv(StringInfo cmdline, const char *argv[]) -{ - /* argv0 is required */ - Assert(*argv != NULL && **argv != '\0'); - QuoteWindowsArgvElement(cmdline, *argv, false); - ++argv; - - for (; *argv != NULL; ++argv) - { - appendStringInfoChar(cmdline, ' '); - QuoteWindowsArgvElement(cmdline, *argv, false); - } -} - -/* - * Run a process on Windows and wait for it to exit, then return its exit code. - * Preserve argument quoting. See exec_cmd() for the function contract details. - * This is only split out to keep all the win32 horror separate for reability. - * - * Don't be tempted to use Win32's _spawnv. It is not like execv. It does *not* - * preserve the individual arguments in the vector, it concatenates them - * without any escaping or quoting. Thus any arguments with spaces, double - * quotes, etc will be mangled by the child process's MSVC runtime when it - * tries to turn the argument string back into an argument vector for the main - * function by calling CommandLineToArgv() from the C library entrypoint. - * _spawnv is also limited to 1024 characters not the 32767 characters permited - * by the underlying Win32 APIs, and that could matter for pg_dump. - * - * This provides something more like we'e expect from execv and waitpid() - * including a waitpid()-style return code with the exit code in the high - * 8 bits of a 16 bit value. Use WEXITSTATUS() for the exit status. The - * special value -1 is returned for a failure to launch the process, - * wait for it, or get its exit code. - */ -static int -exec_cmd_win32(const char *cmd, char *cmdargv[]) -{ - BOOL ret; - int exitcode = -1; - PROCESS_INFORMATION pi; - - elog(DEBUG1, "trying to launch \"%s\"", cmd); - - /* Launch the process */ - { - STARTUPINFO si; - StringInfoData cmdline; - char *cmd_tmp; - - /* Deal with insane windows command line quoting */ - initStringInfo(&cmdline); - QuoteWindowsArgv(&cmdline, cmdargv); - - /* CreateProcess may scribble on the cmd string */ - cmd_tmp = pstrdup(cmd); - - /* - * STARTUPINFO contains various extra options for the process that are - * not passed as CreateProcess flags, and is required. - */ - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - - /* - * PROCESS_INFORMATION accepts the returned process handle. - */ - ZeroMemory(&pi, sizeof(pi)); - ret = CreateProcess(cmd_tmp, cmdline.data, - NULL /* default process attributes */ , - NULL /* default thread attributes */ , - TRUE /* handles (fds) are inherited, to match - * execv */ , - CREATE_NO_WINDOW /* process creation flags */ , - NULL /* inherit environment variables */ , - NULL /* inherit working directory */ , - &si, - &pi); - - pfree(cmd_tmp); - pfree(cmdline.data); - } - - if (!ret) - { - char *winerr = PglGetLastWin32Error(); - - ereport(LOG, - (errcode_for_file_access(), - errmsg("failed to launch \"%s\": %s", - cmd, winerr))); - pfree(winerr); - } - else - { - /* - * Process created. It can still fail due to DLL linkage errors, - * startup problems etc, but the handle exists. - * - * Wait for it to exit, while responding to interrupts. Ideally we - * should be able to use WaitEventSetWait here since Windows sees a - * process handle much like a socket, but the Pg API for it won't let - * us, so we have to DIY. - */ - - elog(DEBUG1, "process launched, waiting"); - - do - { - ret = WaitForSingleObject(pi.hProcess, 500 /* timeout in ms */ ); - - /* - * Note that if we elog(ERROR) or elog(FATAL) as a result of a - * signal here we won't kill the child proc. - */ - CHECK_FOR_INTERRUPTS(); - - if (ret == WAIT_TIMEOUT) - continue; - - if (ret != WAIT_OBJECT_0) - { - char *winerr = PglGetLastWin32Error(); - - ereport(DEBUG1, - (errcode_for_file_access(), - errmsg("unexpected WaitForSingleObject() return code %d while waiting for child process \"%s\": %s", - ret, cmd, winerr))); - pfree(winerr); - /* Try to get the exit code anyway */ - } - - if (!GetExitCodeProcess(pi.hProcess, &exitcode)) - { - char *winerr = PglGetLastWin32Error(); - - ereport(DEBUG1, - (errcode_for_file_access(), - errmsg("failed to get exit code from process \"%s\": %s", - cmd, winerr))); - pfree(winerr); - /* Give up on learning about the process's outcome */ - exitcode = -1; - break; - } - else - { - /* Woken up for a reason other than child process termination */ - if (exitcode == STILL_ACTIVE) - continue; - - /* - * Process must've exited, so code is a value from - * ExitProcess, TerminateProcess, main or WinMain. - */ - ereport(DEBUG1, - (errmsg("process \"%s\" exited with code %d", - cmd, exitcode))); - - /* - * Adapt exit code to WEXITSTATUS form to behave like - * waitpid(). - * - * The lower 8 bits are the terminating signal, with 0 for no - * signal. - */ - exitcode = exitcode << 8; - - break; - } - } while (true); - - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - } - - elog(DEBUG1, "exec_cmd_win32 for \"%s\" exiting with %d", cmd, exitcode); - return exitcode; -} -#endif diff --git a/utils/pgindent/typedefs.list b/utils/pgindent/typedefs.list index d05b8a10..6cae03e6 100644 --- a/utils/pgindent/typedefs.list +++ b/utils/pgindent/typedefs.list @@ -234,7 +234,6 @@ BTVacuumPostingData BTWriteState BUF_MEM BYTE -BY_HANDLE_FILE_INFORMATION BackendParameters BackendStartupData BackendState @@ -605,7 +604,6 @@ DR_transientrel DSMREntryType DSMRegistryCtxStruct DSMRegistryEntry -DWORD DataDirSyncMethod DataDumperPtr DataPageDeleteStack @@ -1137,7 +1135,6 @@ GucStackState GucStringAssignHook GucStringCheckHook GzipCompressorState -HANDLE HASHACTION HASHBUCKET HASHCTL @@ -1525,7 +1522,6 @@ LPSERVICE_STATUS LPSTR LPTHREAD_START_ROUTINE LPTSTR -LPVOID LPWSTR LSEG LUID @@ -2620,7 +2616,6 @@ RunMode RunningTransactions RunningTransactionsData SASLStatus -SC_HANDLE SECURITY_ATTRIBUTES SECURITY_STATUS SEG @@ -2628,7 +2623,6 @@ SERIALIZABLEXACT SERIALIZABLEXID SERIALIZABLEXIDTAG SERVICE_STATUS -SERVICE_STATUS_HANDLE SERVICE_TABLE_ENTRY SID_AND_ATTRIBUTES SID_IDENTIFIER_AUTHORITY @@ -4245,7 +4239,6 @@ walrcv_server_version_fn walrcv_startstreaming_fn wchar2mb_with_len_converter wchar_t -win32_deadchild_waitinfo wint_t worker_state worktable From ec801fa624f091b04d2f731ef441eb46bd5409ec Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 24 Feb 2026 11:44:57 +0100 Subject: [PATCH 3/4] Expand SPOCK_CT_INSERT_EXISTS comment with check_all_uc_indexes semantics --- include/spock_conflict.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/include/spock_conflict.h b/include/spock_conflict.h index ba196579..42f457d7 100644 --- a/include/spock_conflict.h +++ b/include/spock_conflict.h @@ -50,7 +50,16 @@ extern bool spock_save_resolutions; */ typedef enum { - /* The row to be inserted violates unique constraint */ + /* + * The row to be inserted violates a unique constraint. + * This behaviour is controlled by GUC check_all_uc_indexes (default OFF). + * When ON, this conflict exactly matches PostgreSQL's INSERT_EXISTS + * conflict and Spock attempts to resolve it when any UNIQUE index rejects + * the insertion. It is counted as a conflict in subscription statistics + * and the exception log. When OFF, only a replica identity index + * violation is counted as a conflict; other index violations cause an + * ERROR and are not counted as conflicts. + */ SPOCK_CT_INSERT_EXISTS = 0, /* The row to be updated was modified by a different origin */ From e5d44bdaa3922ced7e1342135f43efd156755064 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 31 Mar 2026 14:01:09 +0200 Subject: [PATCH 4/4] Add clarification comment for spock_disable_subscription It may be unclear to detect if someone forget to commit after the call of this function wiping out the result. So, add a comment to reduce number of coding errors. --- src/spock_exception_handler.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/spock_exception_handler.c b/src/spock_exception_handler.c index 811bf469..140b7e4e 100644 --- a/src/spock_exception_handler.c +++ b/src/spock_exception_handler.c @@ -217,6 +217,12 @@ add_entry_to_exception_log(Oid remote_origin, TimestampTz remote_commit_ts, * This function is invoked when the configured exception handling behavior is * SUB_DISABLE, meaning the subscription must be suspended instead of skipping * or retrying the failing transaction. + * + * May be called with or without an active transaction. If no transaction is + * in progress, one is started and committed internally. If the caller already + * holds an open transaction, it is the caller's responsibility to ensure that + * transaction is either committed or terminates with a FATAL error; otherwise + * the subscription state change and exception_log entry will be rolled back. */ void spock_disable_subscription(SpockSubscription *sub,